From b4b1a2c11b119a1977c22368f03ec41ea1ce28bf Mon Sep 17 00:00:00 2001 From: fuzhiqiang Date: Sun, 16 Jun 2024 18:02:45 +0800 Subject: [PATCH] deploy --- 404.html | 2 +- ...\346\234\200\344\274\230\350\247\243.html" | 2 +- article/cms.html | 2 +- ...nd-engineering_performance.md.e4e7bf85.js} | 1062 ++++++++-------- ...gineering_performance.md.e4e7bf85.lean.js} | 2 +- fe-utils/git.html | 2 +- ...\345\267\245\345\205\267\345\272\223.html" | 2 +- fe-utils/tool.html | 2 +- fragment/Monorepo.html | 2 +- fragment/api-no-repeat.html | 2 +- fragment/auto-try-catch.html | 2 +- fragment/babel-console.html | 2 +- fragment/const.html | 2 +- fragment/disable-debugger.html | 2 +- fragment/fetch-pause.html | 2 +- fragment/fetch.html | 2 +- fragment/forEach.html | 2 +- fragment/nextTick.html | 2 +- fragment/npm-scripts.html | 2 +- fragment/promise-cancel.html | 2 +- fragment/react-duplicate.html | 2 +- fragment/react-hooks-timer.html | 2 +- fragment/react-useState.html | 2 +- fragment/return-await.html | 2 +- fragment/setTimeout.html | 2 +- fragment/tree-shaking.html | 2 +- fragment/useRequest.html | 2 +- fragment/var-array.html | 2 +- fragment/video.html | 2 +- ...\346\240\270\346\236\266\346\236\204.html" | 2 +- ...\345\217\243\350\256\276\350\256\241.html" | 2 +- "fragment/\346\262\231\347\233\222.html" | 2 +- "fragment/\351\273\221\347\231\275.html" | 2 +- ...\220\206\345\231\250\344\271\213SCSS.html" | 2 +- ...\345\267\245\347\250\213\345\214\226.html" | 2 +- front-end-engineering/PackageManager.html | 2 +- .../engineering-onepage.html | 2 +- front-end-engineering/jscompatibility.html | 2 +- front-end-engineering/modularization.html | 2 +- front-end-engineering/node.html | 2 +- front-end-engineering/performance.html | 1066 +++++++++-------- .../pnpm\345\216\237\347\220\206.html" | 2 +- front-end-engineering/theme.html | 2 +- front-end-engineering/webpack5-mf.html | 2 +- ...\347\224\250\346\213\223\345\261\225.html" | 2 +- ...\345\267\245\347\250\213\345\214\226.html" | 2 +- ...\345\244\204\347\220\206\345\231\250.html" | 2 +- getting-started.html | 2 +- hashmap.json | 2 +- html-css/CSS.html | 2 +- html-css/HTML.html | 2 +- html-css/animation.html | 2 +- html-css/canvas-svg.html | 2 +- html-css/drag.html | 2 +- html-css/flex.html | 2 +- html-css/interview.html | 2 +- html-css/principle.html | 2 +- html-css/selector.html | 2 +- html-css/temop.html | 2 +- index.html | 2 +- ...\346\263\225\347\254\224\350\257\225.html" | 2 +- ...\351\227\256\346\210\221\345\220\227.html" | 2 +- ...\344\270\216\345\217\215\345\260\204.html" | 2 +- ...\346\255\245\345\244\204\347\220\206.html" | 2 +- ...\347\224\237\346\210\220\345\231\250.html" | 2 +- react/Fiber.html | 2 +- react/ReactRouter.html | 2 +- react/Redux.html | 2 +- react/component-communication.html | 2 +- react/context.html | 2 +- react/dva.html | 2 +- react/event.html | 2 +- react/hooks.html | 2 +- react/index.html | 2 +- react/lifecycle.html | 2 +- react/react-interview.html | 2 +- react/react-redux-router.html | 2 +- react/render.html | 2 +- react/transition.html | 2 +- react/umi.html | 2 +- react/utils.html | 2 +- ts/TypeScript-onePage.html | 2 +- vue/SSR.html | 2 +- vue/challages.html | 2 +- vue/component-communication.html | 2 +- vue/computed.html | 2 +- vue/diff.html | 2 +- vue/directive.html | 2 +- vue/interviewer.html | 2 +- vue/keep-alive-LRU.html | 2 +- vue/lifecycle.html | 2 +- vue/nextTick.html | 2 +- vue/reactive.html | 2 +- vue/slot.html | 2 +- vue/v-model.html | 2 +- vue/vdom.html | 2 +- vue/vs.html | 2 +- vue/vue-cli.html | 2 +- vue/vue-compile.html | 2 +- vue/vue-interview.html | 2 +- vue/vue-router.html | 2 +- vue/vue3-onepage.html | 2 +- vue/vuex.html | 2 +- 103 files changed, 1209 insertions(+), 1121 deletions(-) rename assets/{front-end-engineering_performance.md.08504e29.js => front-end-engineering_performance.md.e4e7bf85.js} (63%) rename assets/{front-end-engineering_performance.md.08504e29.lean.js => front-end-engineering_performance.md.e4e7bf85.lean.js} (53%) diff --git a/404.html b/404.html index d1142de3..98d0bbd5 100644 --- a/404.html +++ b/404.html @@ -12,7 +12,7 @@
Skip to content

404

PAGE NOT FOUND

But if you don't change your direction, and if you keep looking, you may end up where you are heading.
- + diff --git "a/algorithm/\360\237\224\245\345\210\267\351\242\230\344\271\213\346\216\242\347\264\242\346\234\200\344\274\230\350\247\243.html" "b/algorithm/\360\237\224\245\345\210\267\351\242\230\344\271\213\346\216\242\347\264\242\346\234\200\344\274\230\350\247\243.html" index 2a795f7e..3bda5aaf 100644 --- "a/algorithm/\360\237\224\245\345\210\267\351\242\230\344\271\213\346\216\242\347\264\242\346\234\200\344\274\230\350\247\243.html" +++ "b/algorithm/\360\237\224\245\345\210\267\351\242\230\344\271\213\346\216\242\347\264\242\346\234\200\344\274\230\350\247\243.html" @@ -87,7 +87,7 @@ }

总结:在解决问题的基础上,再进行优化。方法一使用二分复杂度为O(nlogn),使用窗口法就可以将复杂度将为O(n)。

- + diff --git a/article/cms.html b/article/cms.html index e522f9d6..1df175a8 100644 --- a/article/cms.html +++ b/article/cms.html @@ -13,7 +13,7 @@
Skip to content
On this page

一站式-后台前端解决方案调研

TIP

搜索方式:github 搜名称

Hooks-Admin

react-admin

vue-element-admin

Antd Pro Vue - 背靠阿里,代码过硬,大型项目首选

Vue vben admin - 宝藏后台管理 基于 Vue3 UI清新 功能扎实

Naive Ui admin 适合小项目

vue3-antd-admin

gin-vue-admin

vue-pure-admin

vue3-composition-admin

vue-admin-perfect

gin-vue-admin

vue-vben-admin

Geeker-Admin

soybean-admin

vue-admin-box

vue-next-admin

vue-admin-better

v3-admin-vite

vue-manage-system

vue3-admin-plus

https://github.com/RainManGO/vue3-composition-admin

https://github.com/jzfai/vue3-admin-plus

https://github.com/tobe-fe-dalao/fast-vue3

https://github.com/ibwei/vue3-ts-base

https://github.com/jzfai/vue3-admin-ts

https://github.com/zouzhibin/vue-admin-perfect

- + diff --git a/assets/front-end-engineering_performance.md.08504e29.js b/assets/front-end-engineering_performance.md.e4e7bf85.js similarity index 63% rename from assets/front-end-engineering_performance.md.08504e29.js rename to assets/front-end-engineering_performance.md.e4e7bf85.js index d053b8b3..9c72e8d2 100644 --- a/assets/front-end-engineering_performance.md.08504e29.js +++ b/assets/front-end-engineering_performance.md.e4e7bf85.js @@ -1,69 +1,69 @@ -import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets/2024-01-27-11-37-14.86d37b7f.png",o="/blog/assets/2024-01-27-11-42-53.5bf914cc.png",e="/blog/assets/2024-01-27-11-43-39.3f42a6fd.png",c="/blog/assets/2024-01-27-11-43-48.dc786c83.png",r="/blog/assets/2024-01-27-11-45-28.86477da9.png",t="/blog/assets/2024-01-28-16-38-33.db813e08.png",B="/blog/assets/2024-01-28-16-43-50.943cbfac.png",y="/blog/assets/2024-01-28-16-45-11.6c1597c5.png",i="/blog/assets/2024-01-28-16-52-03.61de8fe4.png",F="/blog/assets/2024-01-28-16-52-40.1ee5db60.png",u="/blog/assets/2024-01-28-17-09-16.389ef971.png",d="/blog/assets/2024-01-28-17-09-22.8a8ef8fe.png",b="/blog/assets/2024-01-28-17-18-12.4f54b024.png",A="/blog/assets/2024-01-28-17-28-28.709acd8d.png",m="/blog/assets/2024-01-28-17-29-13.fbcf0c26.png",C="/blog/assets/2024-01-28-17-30-23.3db19902.png",_=JSON.parse('{"title":"前端性能优化方法论","description":"","frontmatter":{},"headers":[{"level":2,"title":"Webpack 优化","slug":"webpack-优化","link":"#webpack-优化","children":[{"level":3,"title":"1. 构建性能","slug":"_1-构建性能","link":"#_1-构建性能","children":[]},{"level":3,"title":"2. 传输性能","slug":"_2-传输性能","link":"#_2-传输性能","children":[]},{"level":3,"title":"3. 运行性能","slug":"_3-运行性能","link":"#_3-运行性能","children":[]},{"level":3,"title":"4. webpack5 内置优化","slug":"_4-webpack5-内置优化","link":"#_4-webpack5-内置优化","children":[]},{"level":3,"title":"字节跳动面试题:说一下项目里有做过哪些 webpack 上的优化","slug":"字节跳动面试题-说一下项目里有做过哪些-webpack-上的优化","link":"#字节跳动面试题-说一下项目里有做过哪些-webpack-上的优化","children":[]}]},{"level":2,"title":"CSS","slug":"css","link":"#css","children":[]},{"level":2,"title":"网络层面","slug":"网络层面","link":"#网络层面","children":[{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]},{"level":3,"title":"CDN","slug":"cdn","link":"#cdn","children":[]},{"level":3,"title":"增加带宽","slug":"增加带宽","link":"#增加带宽","children":[]},{"level":3,"title":"http内置优化","slug":"http内置优化","link":"#http内置优化","children":[]},{"level":3,"title":"数据传输层面","slug":"数据传输层面","link":"#数据传输层面","children":[]}]},{"level":2,"title":"Vue","slug":"vue","link":"#vue","children":[{"level":3,"title":"Vue 开发优化","slug":"vue-开发优化","link":"#vue-开发优化","children":[]},{"level":3,"title":"Vue3 内置优化","slug":"vue3-内置优化","link":"#vue3-内置优化","children":[]},{"level":3,"title":"问题梳理","slug":"问题梳理","link":"#问题梳理","children":[]}]},{"level":2,"title":"React","slug":"react","link":"#react","children":[{"level":3,"title":"总结","slug":"总结-1","link":"#总结-1","children":[]}]},{"level":2,"title":"高性能JavaScript","slug":"高性能javascript","link":"#高性能javascript","children":[{"level":3,"title":"开发注意","slug":"开发注意","link":"#开发注意","children":[]},{"level":3,"title":"懒加载","slug":"懒加载","link":"#懒加载","children":[]},{"level":3,"title":"回流与重绘","slug":"回流与重绘","link":"#回流与重绘","children":[]},{"level":3,"title":"如何对项目中的图片进行优化?","slug":"如何对项目中的图片进行优化","link":"#如何对项目中的图片进行优化","children":[]}]},{"level":2,"title":"优化首屏响应","slug":"优化首屏响应","link":"#优化首屏响应","children":[{"level":3,"title":"觉得快","slug":"觉得快","link":"#觉得快","children":[]},{"level":3,"title":"真实快","slug":"真实快","link":"#真实快","children":[]}]}],"relativePath":"front-end-engineering/performance.md","lastUpdated":1710671080000}'),h={name:"front-end-engineering/performance.md"},g=l('

前端性能优化方法论

我们可以从两个方面来看性能优化的意义:

  1. 用户角度:网站优化能够让页面加载得更快,响应更加及时,极大提升用户体验。
  2. 服务商角度:优化会减少页面资源请求数,减小请求资源所占带宽大小,从而节省可观的带宽资源。

网站优化的目标就是减少网站加载时间,提高响应速度。 Google 和亚马逊的研究表明,Google 页面加载的时间从 0.4 秒提升到 0.9 秒导致丢失了 20% 流量和广告收入,对于亚马逊,页面加载时间每增加 100ms 就意味着 1% 的销售额损失。 可见,页面的加载速度对于用户有着至关重要的影响。

Webpack 优化

如何分析打包结果?webpack-bundle-analyzer

1. 构建性能

TIP

这里所说的构建性能,是指在开发阶段的构建性能,而不是生产环境的构建性能

优化的目标,是降低从打包开始,到代码效果呈现所经过的时间

构建性能会影响开发效率。构建性能越高,开发过程中时间的浪费越少

1.1 减少模块解析

模块解析包括:抽象语法树分析、依赖分析、模块语法替换

如果某个模块不做解析,该模块经过loader处理后的代码就是最终代码。

如果没有loader对该模块进行处理,该模块的源码就是最终打包结果的代码。

如果不对某个模块进行解析,可以缩短构建时间,那么哪些模块不需要解析呢?

模块中无其他依赖:一些已经打包好的第三方库,比如jquery,所以可以配置module.noParse,它是一个正则,被正则匹配到的模块不会解析

js
module.exports = {
-  mode: 'development',
+import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets/2024-01-27-11-37-14.86d37b7f.png",o="/blog/assets/2024-01-27-11-42-53.5bf914cc.png",e="/blog/assets/2024-01-27-11-43-39.3f42a6fd.png",c="/blog/assets/2024-01-27-11-43-48.dc786c83.png",r="/blog/assets/2024-01-27-11-45-28.86477da9.png",t="/blog/assets/2024-01-28-16-38-33.db813e08.png",B="/blog/assets/2024-01-28-16-43-50.943cbfac.png",y="/blog/assets/2024-01-28-16-45-11.6c1597c5.png",i="/blog/assets/2024-01-28-16-52-03.61de8fe4.png",F="/blog/assets/2024-01-28-16-52-40.1ee5db60.png",u="/blog/assets/2024-01-28-17-09-16.389ef971.png",d="/blog/assets/2024-01-28-17-09-22.8a8ef8fe.png",b="/blog/assets/2024-01-28-17-18-12.4f54b024.png",A="/blog/assets/2024-01-28-17-28-28.709acd8d.png",m="/blog/assets/2024-01-28-17-29-13.fbcf0c26.png",C="/blog/assets/2024-01-28-17-30-23.3db19902.png",_=JSON.parse('{"title":"前端性能优化方法论","description":"","frontmatter":{},"headers":[{"level":2,"title":"Webpack 优化","slug":"webpack-优化","link":"#webpack-优化","children":[{"level":3,"title":"1. 构建性能","slug":"_1-构建性能","link":"#_1-构建性能","children":[]},{"level":3,"title":"2. 传输性能","slug":"_2-传输性能","link":"#_2-传输性能","children":[]},{"level":3,"title":"3. 运行性能","slug":"_3-运行性能","link":"#_3-运行性能","children":[]},{"level":3,"title":"4. webpack5 内置优化","slug":"_4-webpack5-内置优化","link":"#_4-webpack5-内置优化","children":[]},{"level":3,"title":"字节跳动面试题:说一下项目里有做过哪些 webpack 上的优化","slug":"字节跳动面试题-说一下项目里有做过哪些-webpack-上的优化","link":"#字节跳动面试题-说一下项目里有做过哪些-webpack-上的优化","children":[]}]},{"level":2,"title":"CSS","slug":"css","link":"#css","children":[]},{"level":2,"title":"网络层面","slug":"网络层面","link":"#网络层面","children":[{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]},{"level":3,"title":"CDN","slug":"cdn","link":"#cdn","children":[]},{"level":3,"title":"增加带宽","slug":"增加带宽","link":"#增加带宽","children":[]},{"level":3,"title":"http 内置优化","slug":"http-内置优化","link":"#http-内置优化","children":[]},{"level":3,"title":"数据传输层面","slug":"数据传输层面","link":"#数据传输层面","children":[]}]},{"level":2,"title":"Vue","slug":"vue","link":"#vue","children":[{"level":3,"title":"Vue 开发优化","slug":"vue-开发优化","link":"#vue-开发优化","children":[]},{"level":3,"title":"Vue3 内置优化","slug":"vue3-内置优化","link":"#vue3-内置优化","children":[]},{"level":3,"title":"问题梳理","slug":"问题梳理","link":"#问题梳理","children":[]}]},{"level":2,"title":"React","slug":"react","link":"#react","children":[{"level":3,"title":"总结","slug":"总结-1","link":"#总结-1","children":[]}]},{"level":2,"title":"高性能 JavaScript","slug":"高性能-javascript","link":"#高性能-javascript","children":[{"level":3,"title":"开发注意","slug":"开发注意","link":"#开发注意","children":[]},{"level":3,"title":"懒加载","slug":"懒加载","link":"#懒加载","children":[]},{"level":3,"title":"回流与重绘","slug":"回流与重绘","link":"#回流与重绘","children":[]},{"level":3,"title":"如何对项目中的图片进行优化?","slug":"如何对项目中的图片进行优化","link":"#如何对项目中的图片进行优化","children":[]}]},{"level":2,"title":"优化首屏响应","slug":"优化首屏响应","link":"#优化首屏响应","children":[{"level":3,"title":"觉得快","slug":"觉得快","link":"#觉得快","children":[]},{"level":3,"title":"真实快","slug":"真实快","link":"#真实快","children":[]}]}],"relativePath":"front-end-engineering/performance.md","lastUpdated":1718532121000}'),h={name:"front-end-engineering/performance.md"},g=l('

前端性能优化方法论

https://juejin.cn/post/6993137683841155080

我们可以从两个方面来看性能优化的意义:

  1. 用户角度:网站优化能够让页面加载得更快,响应更加及时,极大提升用户体验。
  2. 服务商角度:优化会减少页面资源请求数,减小请求资源所占带宽大小,从而节省可观的带宽资源。

网站优化的目标就是减少网站加载时间,提高响应速度。 Google 和亚马逊的研究表明,Google 页面加载的时间从 0.4 秒提升到 0.9 秒导致丢失了 20% 流量和广告收入,对于亚马逊,页面加载时间每增加 100ms 就意味着 1% 的销售额损失。 可见,页面的加载速度对于用户有着至关重要的影响。

Webpack 优化

如何分析打包结果?webpack-bundle-analyzer

1. 构建性能

TIP

这里所说的构建性能,是指在开发阶段的构建性能,而不是生产环境的构建性能

优化的目标,是降低从打包开始,到代码效果呈现所经过的时间

构建性能会影响开发效率。构建性能越高,开发过程中时间的浪费越少

1.1 减少模块解析

模块解析包括:抽象语法树分析、依赖分析、模块语法替换

如果某个模块不做解析,该模块经过 loader 处理后的代码就是最终代码。

如果没有 loader 对该模块进行处理,该模块的源码就是最终打包结果的代码。

如果不对某个模块进行解析,可以缩短构建时间,那么哪些模块不需要解析呢?

模块中无其他依赖:一些已经打包好的第三方库,比如 jquery,所以可以配置 module.noParse,它是一个正则,被正则匹配到的模块不会解析

js
module.exports = {
+  mode: "development",
   module: {
-    noParse: /jquery/
-  }
-}
+    noParse: /jquery/,
+  },
+};
 
module.exports = {
-  mode: 'development',
+  mode: "development",
   module: {
-    noParse: /jquery/
-  }
-}
-

1.2 优化loader性能

  1. 进一步限制loader的应用范围。对于某些库,不使用loader

例如:babel-loader可以转换ES6或更高版本的语法,可是有些库本身就是用ES5语法书写的,不需要转换,使用babel-loader反而会浪费构建时间 lodash就是这样的一个库,lodash是在ES5之前出现的库,使用的是ES3语法 通过module.rule.exclude或module.rule.include,排除或仅包含需要应用loader的场景

js
module.exports = {
-    module: {
-        rules: [
-            {
-                test: /\\.js$/,
-                exclude: /lodash/,
-                use: "babel-loader"
-            }
-        ]
-    }
-}
+    noParse: /jquery/,
+  },
+};
+

1.2 优化 loader 性能

  1. 进一步限制 loader 的应用范围。对于某些库,不使用 loader

例如:babel-loader 可以转换 ES6 或更高版本的语法,可是有些库本身就是用 ES5 语法书写的,不需要转换,使用 babel-loader 反而会浪费构建时间 lodash 就是这样的一个库,lodash 是在 ES5 之前出现的库,使用的是 ES3 语法 通过 module.rule.exclude 或 module.rule.include,排除或仅包含需要应用 loader 的场景

js
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\\.js$/,
+        exclude: /lodash/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
 
module.exports = {
-    module: {
-        rules: [
-            {
-                test: /\\.js$/,
-                exclude: /lodash/,
-                use: "babel-loader"
-            }
-        ]
-    }
-}
-

如果暴力一点,甚至可以排除掉node_modules目录中的模块,或仅转换src目录的模块

js
module.exports = {
-    module: {
-        rules: [
-            {
-                test: /\\.js$/,
-                exclude: /node_modules/,
-                //或
-                // include: /src/,
-                use: "babel-loader"
-            }
-        ]
-    }
-}
+  module: {
+    rules: [
+      {
+        test: /\\.js$/,
+        exclude: /lodash/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
+

如果暴力一点,甚至可以排除掉 node_modules 目录中的模块,或仅转换 src 目录的模块

js
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\\.js$/,
+        exclude: /node_modules/,
+        //或
+        // include: /src/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
 
module.exports = {
-    module: {
-        rules: [
-            {
-                test: /\\.js$/,
-                exclude: /node_modules/,
-                //或
-                // include: /src/,
-                use: "babel-loader"
-            }
-        ]
-    }
-}
-

这种做法是对loader的范围进行进一步的限制,和noParse不冲突

  1. 缓存loader的结果

我们可以基于一种假设:如果某个文件内容不变,经过相同的loader解析后,解析后的结果也不变 于是,可以将loader的解析结果保存下来,让后续的解析直接使用保存的结果 cache-loader可以实现这样的功能:第一次打包会慢,因为有缓存的过程,以后就快了

js
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\\.js$/,
+        exclude: /node_modules/,
+        //或
+        // include: /src/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
+

这种做法是对 loader 的范围进行进一步的限制,和 noParse 不冲突

  1. 缓存 loader 的结果

我们可以基于一种假设:如果某个文件内容不变,经过相同的 loader 解析后,解析后的结果也不变 于是,可以将 loader 的解析结果保存下来,让后续的解析直接使用保存的结果 cache-loader 可以实现这样的功能:第一次打包会慢,因为有缓存的过程,以后就快了

js
module.exports = {
   module: {
     rules: [
       {
         test: /\\.js$/,
-        use: ['cache-loader', ...loaders]
+        use: ["cache-loader", ...loaders],
       },
     ],
   },
@@ -73,12 +73,12 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     rules: [
       {
         test: /\\.js$/,
-        use: ['cache-loader', ...loaders]
+        use: ["cache-loader", ...loaders],
       },
     ],
   },
 };
-

有趣的是,cache-loader放到最前面,却能够决定后续的loader是否运行。实际上,loader的运行过程中,还包含一个过程,即pitch

cache-loader还可以实现各自自定义的配置,具体方式见文档

  1. 为loader的运行开启多线程

thread-loader会开启一个线程池,线程池中包含适量的线程

它会把后续的loader放到线程池的线程中运行,以提高构建效率。由于后续的loader会放到新的线程中,所以,后续的loader不能:

  • 使用 webpack api 生成文件
  • 无法使用自定义的 plugin api
  • 无法访问 webpack options

在实际的开发中,可以进行测试,来决定thread-loader放到什么位置

特别注意,开启和管理线程需要消耗时间,在小型项目中使用thread-loader反而会增加构建时间

HappyPack:受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。

HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了

js
module: {
+

有趣的是,cache-loader 放到最前面,却能够决定后续的 loader 是否运行。实际上,loader 的运行过程中,还包含一个过程,即 pitch

cache-loader 还可以实现各自自定义的配置,具体方式见文档

  1. 为 loader 的运行开启多线程

thread-loader 会开启一个线程池,线程池中包含适量的线程

它会把后续的 loader 放到线程池的线程中运行,以提高构建效率。由于后续的 loader 会放到新的线程中,所以,后续的 loader 不能:

  • 使用 webpack api 生成文件
  • 无法使用自定义的 plugin api
  • 无法访问 webpack options

在实际的开发中,可以进行测试,来决定 thread-loader 放到什么位置

特别注意,开启和管理线程需要消耗时间,在小型项目中使用 thread-loader 反而会增加构建时间

HappyPack:受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。

HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了

js
module: {
   loaders: [
     {
       test: /\\.js$/,
@@ -116,35 +116,37 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     threads: 4
   })
 ]
-

1.3 热替换 HMR

热替换并不能降低构建时间(可能还会稍微增加),但可以降低代码改动到效果呈现的时间 当使用webpack-dev-server时,考虑代码改动到效果呈现的过程

原理

  1. 更改配置
js
module.exports = {
-  devServer:{
-    hot:true // 开启HMR
+

1.3 热替换 HMR

热替换并不能降低构建时间(可能还会稍微增加),但可以降低代码改动到效果呈现的时间 当使用 webpack-dev-server 时,考虑代码改动到效果呈现的过程

原理

  1. 更改配置
js
module.exports = {
+  devServer: {
+    hot: true, // 开启HMR
   },
-  plugins:[ 
+  plugins: [
     // 可选
-    new webpack.HotModuleReplacementPlugin()
-  ]
-}
+    new webpack.HotModuleReplacementPlugin(),
+  ],
+};
 
module.exports = {
-  devServer:{
-    hot:true // 开启HMR
+  devServer: {
+    hot: true, // 开启HMR
   },
-  plugins:[ 
+  plugins: [
     // 可选
-    new webpack.HotModuleReplacementPlugin()
-  ]
-}
+    new webpack.HotModuleReplacementPlugin(),
+  ],
+};
 
  1. 更改代码
js
// index.js
 
-if(module.hot){ // 是否开启了热更新
-  module.hot.accept() // 接受热更新
+if (module.hot) {
+  // 是否开启了热更新
+  module.hot.accept(); // 接受热更新
 }
 
// index.js
 
-if(module.hot){ // 是否开启了热更新
-  module.hot.accept() // 接受热更新
+if (module.hot) {
+  // 是否开启了热更新
+  module.hot.accept(); // 接受热更新
 }
-

首先,这段代码会参与最终运行!当开启了热更新后,webpack-dev-server会向打包结果中注入module.hot属性。默认情况下,webpack-dev-server不管是否开启了热更新,当重新打包后,都会调用location.reload刷新页面

但如果运行了module.hot.accept(),将改变这一行为module.hot.accept()的作用是让webpack-dev-server通过socket管道,把服务器更新的内容发送到浏览器

然后,将结果交给插件HotModuleReplacementPlugin注入的代码执行 插件HotModuleReplacementPlugin会根据覆盖原始代码,然后让代码重新执行 所以,热替换发生在代码运行期

样式热替换

对于样式也是可以使用热替换的,但需要使用style-loader

因为热替换发生时,HotModuleReplacementPlugin只会简单的重新运行模块代码

因此style-loader的代码一运行,就会重新设置style元素中的样式

mini-css-extract-plugin,由于它生成文件是在构建期间,运行期间并会也无法改动文件,因此它对于热替换是无效的

思考:webpack的热更新是如何做到的?说明其原理?

webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

原理:

首先要知道server端和client端都做了处理工作

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

1.4 其他提升构建性能

  1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码
  2. 通过 externals 配置来提取常⽤库
  3. 利⽤ DllPluginDllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的npm包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
  4. 使⽤ Happypack 实现多线程加速编译
  5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度
  6. 使⽤ Tree-shakingScope Hoisting 来剔除多余代码

思考:如何利用webpack来优化前端性能?(提高性能和体验)

用webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运行快速高效。

  • 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用webpack的UglifyJsPlugin和ParallelUglifyPlugin来压缩JS文件, 利用cssnano(css-loader?minimize)来压缩css
  • 利用CDN加速。在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用webpack对于output参数和各loader的publicPath参数来修改资源路径
  • 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数--optimize-minimize来实现
  • 提取公共代码。

思考:如何提高webpack的构建速度?

  1. 多入口情况下,使用CommonsChunkPlugin来提取公共代码
  2. 通过externals配置来提取常用库
  3. 利用DllPlugin和DllReferencePlugin预编译资源模块 通过DllPlugin来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过DllReferencePlugin将预编译的模块加载进来。
  4. 使用Happypack 实现多线程加速编译
  5. 使用webpack-uglify-parallel来提升uglifyPlugin的压缩速度。 原理上webpack-uglify-parallel采用了多核并行压缩来提升压缩速度
  6. 使用Tree-shaking和Scope Hoisting来剔除多余代码

2. 传输性能

TIP

传输性能是指,打包后的JS代码传输到浏览器经过的时间,在优化传输性能时要考虑到:

  1. 总传输量:所有需要传输的JS文件的内容加起来,就是总传输量,重复代码越少,总传输量越少
  2. 文件数量:当访问页面时,需要传输的JS文件数量,文件数量越多,http请求越多,响应速度越慢
  3. 浏览器缓存:JS文件会被浏览器缓存,被缓存的文件不会再进行传输

2.1 手动分包(极大提升构建性能)

默认情况下,vue-cli会利用webpacksrc目录中的所有代码打包成一个bundle

这样就导致访问一个页面时,需要加载所有页面的js代码

我们可以利用webpack对动态import的支持,从而达到把不同页面的代码打包到不同文件中

js
// routes
+

首先,这段代码会参与最终运行!当开启了热更新后,webpack-dev-server会向打包结果中注入module.hot属性。默认情况下,webpack-dev-server不管是否开启了热更新,当重新打包后,都会调用location.reload刷新页面

但如果运行了module.hot.accept(),将改变这一行为module.hot.accept()的作用是让webpack-dev-server通过 socket 管道,把服务器更新的内容发送到浏览器

然后,将结果交给插件HotModuleReplacementPlugin注入的代码执行 插件HotModuleReplacementPlugin会根据覆盖原始代码,然后让代码重新执行 所以,热替换发生在代码运行期

样式热替换

对于样式也是可以使用热替换的,但需要使用style-loader

因为热替换发生时,HotModuleReplacementPlugin只会简单的重新运行模块代码

因此style-loader的代码一运行,就会重新设置 style 元素中的样式

mini-css-extract-plugin,由于它生成文件是在构建期间,运行期间并会也无法改动文件,因此它对于热替换是无效的

思考:webpack 的热更新是如何做到的?说明其原理?

webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

原理:

首先要知道 server 端和 client 端都做了处理工作

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API 对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了 devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

1.4 其他提升构建性能

  1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码
  2. 通过 externals 配置来提取常⽤库
  3. 利⽤ DllPluginDllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的 npm 包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
  4. 使⽤ Happypack 实现多线程加速编译
  5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度
  6. 使⽤ Tree-shakingScope Hoisting 来剔除多余代码

思考:如何利用 webpack 来优化前端性能?(提高性能和体验)

用 webpack 优化前端性能是指优化 webpack 的输出结果,让打包的最终结果在浏览器运行快速高效。

  • 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用 webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS 文件, 利用 cssnano(css-loader?minimize)来压缩 css
  • 利用 CDN 加速。在构建过程中,将引用的静态资源路径修改为 CDN 上对应的路径。可以利用 webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径
  • 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动 webpack 时追加参数--optimize-minimize 来实现
  • 提取公共代码。

思考:如何提高 webpack 的构建速度?

  1. 多入口情况下,使用 CommonsChunkPlugin 来提取公共代码
  2. 通过 externals 配置来提取常用库
  3. 利用 DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引用但是绝对不会修改的 npm 包来进行预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
  4. 使用 Happypack 实现多线程加速编译
  5. 使用 webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采用了多核并行压缩来提升压缩速度
  6. 使用 Tree-shaking 和 Scope Hoisting 来剔除多余代码

2. 传输性能

TIP

传输性能是指,打包后的 JS 代码传输到浏览器经过的时间,在优化传输性能时要考虑到:

  1. 总传输量:所有需要传输的 JS 文件的内容加起来,就是总传输量,重复代码越少,总传输量越少
  2. 文件数量:当访问页面时,需要传输的 JS 文件数量,文件数量越多,http 请求越多,响应速度越慢
  3. 浏览器缓存:JS 文件会被浏览器缓存,被缓存的文件不会再进行传输

2.1 手动分包(极大提升构建性能)

默认情况下,vue-cli会利用webpacksrc目录中的所有代码打包成一个bundle

这样就导致访问一个页面时,需要加载所有页面的 js 代码

我们可以利用 webpack 对动态 import 的支持,从而达到把不同页面的代码打包到不同文件中

js
// routes
 export default [
   {
     name: "Home",
@@ -154,8 +156,8 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
   {
     name: "About",
     path: "/about",
-    component: () => import(/* webpackChunkName: "about" */"@/views/About"),
-  }
+    component: () => import(/* webpackChunkName: "about" */ "@/views/About"),
+  },
 ];
 
// routes
 export default [
@@ -167,100 +169,110 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
   {
     name: "About",
     path: "/about",
-    component: () => import(/* webpackChunkName: "about" */"@/views/About"),
-  }
+    component: () => import(/* webpackChunkName: "about" */ "@/views/About"),
+  },
 ];
-

什么是分包:将一个整体的代码,分布到不同的打包文件中

什么时候要分包?

  • 多个chunk引入了公共模块
  • 公共模块体积较大或较少的变动

基本原理

手动分包的总体思路是:

  1. 先单独的打包公共模块

公共模块会被打包成为动态链接库(dll Dynamic Link Library),并生成资源清单

  1. 根据入口模块进行正常打包

打包时,如果发现模块中使用了资源清单中描述的模块,则不会形成下面的代码结构

js
//源码,入口文件index.js
-import $ from "jquery"
-import _ from "lodash"
+

什么是分包:将一个整体的代码,分布到不同的打包文件中

什么时候要分包?

  • 多个 chunk 引入了公共模块
  • 公共模块体积较大或较少的变动

基本原理

手动分包的总体思路是:

  1. 先单独的打包公共模块

公共模块会被打包成为动态链接库(dll Dynamic Link Library),并生成资源清单

  1. 根据入口模块进行正常打包

打包时,如果发现模块中使用了资源清单中描述的模块,则不会形成下面的代码结构

js
//源码,入口文件index.js
+import $ from "jquery";
+import _ from "lodash";
 _.isArray($(".red"));
 
//源码,入口文件index.js
-import $ from "jquery"
-import _ from "lodash"
+import $ from "jquery";
+import _ from "lodash";
 _.isArray($(".red"));
-

由于资源清单中包含jquery和lodash两个模块,因此打包结果的大致格式是:

js
(function(modules){
+

由于资源清单中包含 jquery 和 lodash 两个模块,因此打包结果的大致格式是:

js
(function (modules) {
   //...
 })({
   // index.js文件的打包结果并没有变化
-  "./src/index.js":
-  function(module, exports, __webpack_require__){
-    var $ = __webpack_require__("./node_modules/jquery/index.js")
-    var _ = __webpack_require__("./node_modules/lodash/index.js")
+  "./src/index.js": function (module, exports, __webpack_require__) {
+    var $ = __webpack_require__("./node_modules/jquery/index.js");
+    var _ = __webpack_require__("./node_modules/lodash/index.js");
     _.isArray($(".red"));
   },
   // 由于资源清单中存在,jquery的代码并不会出现在这里
-  "./node_modules/jquery/index.js":
-  function(module, exports, __webpack_require__){
-    module.exports = jquery;// 直接导出资源清单的名字
+  "./node_modules/jquery/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
+    module.exports = jquery; // 直接导出资源清单的名字
   },
   // 由于资源清单中存在,lodash的代码并不会出现在这里
-  "./node_modules/lodash/index.js":
-  function(module, exports, __webpack_require__){
+  "./node_modules/lodash/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
     module.exports = lodash;
-  }
-})
-
(function(modules){
+  },
+});
+
(function (modules) {
   //...
 })({
   // index.js文件的打包结果并没有变化
-  "./src/index.js":
-  function(module, exports, __webpack_require__){
-    var $ = __webpack_require__("./node_modules/jquery/index.js")
-    var _ = __webpack_require__("./node_modules/lodash/index.js")
+  "./src/index.js": function (module, exports, __webpack_require__) {
+    var $ = __webpack_require__("./node_modules/jquery/index.js");
+    var _ = __webpack_require__("./node_modules/lodash/index.js");
     _.isArray($(".red"));
   },
   // 由于资源清单中存在,jquery的代码并不会出现在这里
-  "./node_modules/jquery/index.js":
-  function(module, exports, __webpack_require__){
-    module.exports = jquery;// 直接导出资源清单的名字
+  "./node_modules/jquery/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
+    module.exports = jquery; // 直接导出资源清单的名字
   },
   // 由于资源清单中存在,lodash的代码并不会出现在这里
-  "./node_modules/lodash/index.js":
-  function(module, exports, __webpack_require__){
+  "./node_modules/lodash/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
     module.exports = lodash;
-  }
-})
-
  1. 打包公共模块

打包公共模块是一个独立的打包过程

  1. 单独打包公共模块,暴露变量名 . npm run dll
js
// webpack.dll.config.js
+  },
+});
+
  1. 打包公共模块

打包公共模块是一个独立的打包过程

  1. 单独打包公共模块,暴露变量名 . npm run dll
js
// webpack.dll.config.js
 module.exports = {
   mode: "production",
   entry: {
-    jquery: ["jquery"],//数组
-    lodash: ["lodash"]
+    jquery: ["jquery"], //数组
+    lodash: ["lodash"],
   },
   output: {
     filename: "dll/[name].js",
-    library: "[name]"// 每个bundle暴露的全局变量名
-  }
+    library: "[name]", // 每个bundle暴露的全局变量名
+  },
 };
 
// webpack.dll.config.js
 module.exports = {
   mode: "production",
   entry: {
-    jquery: ["jquery"],//数组
-    lodash: ["lodash"]
+    jquery: ["jquery"], //数组
+    lodash: ["lodash"],
   },
   output: {
     filename: "dll/[name].js",
-    library: "[name]"// 每个bundle暴露的全局变量名
-  }
+    library: "[name]", // 每个bundle暴露的全局变量名
+  },
 };
 

利用DllPlugin生成资源清单

js
// webpack.dll.config.js
 module.exports = {
   plugins: [
     new webpack.DllPlugin({
       path: path.resolve(__dirname, "dll", "[name].manifest.json"), //资源清单的保存位置
-      name: "[name]"//资源清单中,暴露的变量名
-    })
-  ]
+      name: "[name]", //资源清单中,暴露的变量名
+    }),
+  ],
 };
 
// webpack.dll.config.js
 module.exports = {
   plugins: [
     new webpack.DllPlugin({
       path: path.resolve(__dirname, "dll", "[name].manifest.json"), //资源清单的保存位置
-      name: "[name]"//资源清单中,暴露的变量名
-    })
-  ]
+      name: "[name]", //资源清单中,暴露的变量名
+    }),
+  ],
 };
 

运行后,即可完成公共模块打包

使用公共模块

  1. 在页面中手动引入公共模块
html
<script src="./dll/jquery.js"></script>
 <script src="./dll/lodash.js"></script>
@@ -269,81 +281,83 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
 

重新设置clean-webpack-plugin。如果使用了插件clean-webpack-plugin,为了避免它把公共模块清除,需要做出以下配置

js
new CleanWebpackPlugin({
   // 要清除的文件或目录
   // 排除掉dll目录本身和它里面的文件
-  cleanOnceBeforeBuildPatterns: ["**/*", '!dll', '!dll/*']
-})
+  cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"],
+});
 
new CleanWebpackPlugin({
   // 要清除的文件或目录
   // 排除掉dll目录本身和它里面的文件
-  cleanOnceBeforeBuildPatterns: ["**/*", '!dll', '!dll/*']
-})
-

目录和文件的匹配规则使用的是globbing patterns语法

使用DllReferencePlugin控制打包结果

js
module.exports = {
-  plugins:[// 资源清单
+  cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"],
+});
+

目录和文件的匹配规则使用的是globbing patterns语法

使用 DllReferencePlugin 控制打包结果

js
module.exports = {
+  plugins: [
+    // 资源清单
     new webpack.DllReferencePlugin({
-      manifest: require("./dll/jquery.manifest.json")
+      manifest: require("./dll/jquery.manifest.json"),
     }),
     new webpack.DllReferencePlugin({
-      manifest: require("./dll/lodash.manifest.json")
-    })
-  ]
-}
+      manifest: require("./dll/lodash.manifest.json"),
+    }),
+  ],
+};
 
module.exports = {
-  plugins:[// 资源清单
+  plugins: [
+    // 资源清单
     new webpack.DllReferencePlugin({
-      manifest: require("./dll/jquery.manifest.json")
+      manifest: require("./dll/jquery.manifest.json"),
     }),
     new webpack.DllReferencePlugin({
-      manifest: require("./dll/lodash.manifest.json")
-    })
-  ]
-}
-

总结

手动打包的过程:

  1. 开启output.library暴露公共模块
  2. 用DllPlugin创建资源清单
  3. 用DllReferencePlugin使用资源清单

手动打包的注意事项:

  1. 资源清单不参与运行,可以不放到打包目录中
  2. 记得手动引入公共JS,以及避免被删除
  3. 不要对小型的公共JS库使用

优点:

  1. 极大提升自身模块的打包速度
  2. 极大的缩小了自身文件体积
  3. 有利于浏览器缓存第三方库的公共代码

缺点:

  1. 使用非常繁琐
  2. 如果第三方库中包含重复代码,则效果不太理想

详解dllPlugin

DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin的使用方法如下:

js
// 单独配置在一个文件中
+      manifest: require("./dll/lodash.manifest.json"),
+    }),
+  ],
+};
+

总结

手动打包的过程:

  1. 开启 output.library 暴露公共模块
  2. 用 DllPlugin 创建资源清单
  3. 用 DllReferencePlugin 使用资源清单

手动打包的注意事项:

  1. 资源清单不参与运行,可以不放到打包目录中
  2. 记得手动引入公共 JS,以及避免被删除
  3. 不要对小型的公共 JS 库使用

优点:

  1. 极大提升自身模块的打包速度
  2. 极大的缩小了自身文件体积
  3. 有利于浏览器缓存第三方库的公共代码

缺点:

  1. 使用非常繁琐
  2. 如果第三方库中包含重复代码,则效果不太理想

详解 dllPlugin

DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin 的使用方法如下:

js
// 单独配置在一个文件中
 // webpack.dll.conf.js
-const path = require('path')
-const webpack = require('webpack')
+const path = require("path");
+const webpack = require("webpack");
 module.exports = {
   entry: {
     // 想统一打包的类库
-    vendor: ['react']
+    vendor: ["react"],
   },
   output: {
-    path: path.join(__dirname, 'dist'),
-    filename: '[name].dll.js',
-    library: '[name]-[hash]'
+    path: path.join(__dirname, "dist"),
+    filename: "[name].dll.js",
+    library: "[name]-[hash]",
   },
   plugins: [
     new webpack.DllPlugin({
       // name 必须和 output.library 一致
-      name: '[name]-[hash]',
+      name: "[name]-[hash]",
       // 该属性需要与 DllReferencePlugin 中一致
       context: __dirname,
-      path: path.join(__dirname, 'dist', '[name]-manifest.json')
-    })
-  ]
-}
+      path: path.join(__dirname, "dist", "[name]-manifest.json"),
+    }),
+  ],
+};
 
// 单独配置在一个文件中
 // webpack.dll.conf.js
-const path = require('path')
-const webpack = require('webpack')
+const path = require("path");
+const webpack = require("webpack");
 module.exports = {
   entry: {
     // 想统一打包的类库
-    vendor: ['react']
+    vendor: ["react"],
   },
   output: {
-    path: path.join(__dirname, 'dist'),
-    filename: '[name].dll.js',
-    library: '[name]-[hash]'
+    path: path.join(__dirname, "dist"),
+    filename: "[name].dll.js",
+    library: "[name]-[hash]",
   },
   plugins: [
     new webpack.DllPlugin({
       // name 必须和 output.library 一致
-      name: '[name]-[hash]',
+      name: "[name]-[hash]",
       // 该属性需要与 DllReferencePlugin 中一致
       context: __dirname,
-      path: path.join(__dirname, 'dist', '[name]-manifest.json')
-    })
-  ]
-}
+      path: path.join(__dirname, "dist", "[name]-manifest.json"),
+    }),
+  ],
+};
 

然后需要执行这个配置文件生成依赖文件,接下来需要使用 DllReferencePlugin 将依赖文件引入项目中

js
// webpack.conf.js
 module.exports = {
   // ...省略其他配置
@@ -351,10 +365,10 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     new webpack.DllReferencePlugin({
       context: __dirname,
       // manifest 就是之前打包出来的 json 文件
-      manifest: require('./dist/vendor-manifest.json'),
-    })
-  ]
-}
+      manifest: require("./dist/vendor-manifest.json"),
+    }),
+  ],
+};
 
// webpack.conf.js
 module.exports = {
   // ...省略其他配置
@@ -362,211 +376,219 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     new webpack.DllReferencePlugin({
       context: __dirname,
       // manifest 就是之前打包出来的 json 文件
-      manifest: require('./dist/vendor-manifest.json'),
-    })
-  ]
-}
-

可以通过一些小的优化点来加快打包速度

  • resolve.extensions:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面
  • resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径
  • module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助

2.2 自动分包(会降低构建效率,开发效率提升,新的模块不需要手动处理了)

  1. 基本原理

不同于手动分包,自动分包是从实际的角度出发,从一个更加宏观的角度来控制分包,而一般不对具体哪个包要分出去进行控制

因此使用自动分包,不仅非常方便,而且更加贴合实际的开发需要

要控制自动分包,关键是要配置一个合理的分包策略

有了分包策略之后,不需要额外安装任何插件,webpack会自动的按照策略进行分包

实际上,webpack在内部是使用SplitChunksPlugin进行分包的 过去有一个库CommonsChunkPlugin也可以实现分包,不过由于该库某些地方并不完善,到了webpack4之后,已被SplitChunksPlugin取代

从分包流程中至少可以看出以下几点:

  • 分包策略至关重要,它决定了如何分包
  • 分包时,webpack开启了一个新的chunk,对分离的模块进行打包
  • 打包结果中,公共的部分被提取出来形成了一个单独的文件,它是新chunk的产物
  1. 分包策略的基本配置

webpack提供了optimization配置项,用于配置一些优化信息

其中splitChunks是分包策略的配置

js
module.exports = {
+      manifest: require("./dist/vendor-manifest.json"),
+    }),
+  ],
+};
+

可以通过一些小的优化点来加快打包速度

  • resolve.extensions:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面
  • resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径
  • module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助

2.2 自动分包(会降低构建效率,开发效率提升,新的模块不需要手动处理了)

  1. 基本原理

不同于手动分包,自动分包是从实际的角度出发,从一个更加宏观的角度来控制分包,而一般不对具体哪个包要分出去进行控制

因此使用自动分包,不仅非常方便,而且更加贴合实际的开发需要

要控制自动分包,关键是要配置一个合理的分包策略

有了分包策略之后,不需要额外安装任何插件,webpack 会自动的按照策略进行分包

实际上,webpack 在内部是使用 SplitChunksPlugin 进行分包的 过去有一个库 CommonsChunkPlugin 也可以实现分包,不过由于该库某些地方并不完善,到了 webpack4 之后,已被 SplitChunksPlugin 取代

从分包流程中至少可以看出以下几点:

  • 分包策略至关重要,它决定了如何分包
  • 分包时,webpack 开启了一个新的 chunk,对分离的模块进行打包
  • 打包结果中,公共的部分被提取出来形成了一个单独的文件,它是新 chunk 的产物
  1. 分包策略的基本配置

webpack 提供了 optimization 配置项,用于配置一些优化信息

其中 splitChunks 是分包策略的配置

js
module.exports = {
   optimization: {
     splitChunks: {
       // 分包策略
-    }
-  }
-}
+    },
+  },
+};
 
module.exports = {
   optimization: {
     splitChunks: {
       // 分包策略
-    }
-  }
-}
-

事实上,分包策略有其默认的配置,我们只需要轻微的改动,即可应对大部分分包场景

chunks

该配置项用于配置需要应用分包策略的chunk

我们知道,分包是从已有的chunk中分离出新的chunk,那么哪些chunk需要分离呢

chunks有三个取值,分别是:

  • all: 对于所有的chunk都要应用分包策略
  • async:【默认】仅针对异步chunk应用分包策略
  • initial:仅针对普通chunk应用分包策略

所以,你只需要配置chunks为all即可

maxSize

该配置可以控制包的最大字节数

如果某个包(包括分出来的包)超过了该值,则webpack会尽可能的将其分离成多个包

但是不要忽略的是,分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的,因此,尽管使用了这个配置,完全有可能某个包还是会超过这个体积

另外,该配置看上去很美妙,实际意义其实不大

因为分包的目的是提取大量的公共代码,从而减少总体积和充分利用浏览器缓存

虽然该配置可以把一些包进行再切分,但是实际的总体积和传输量并没有发生变化

如果要进一步减少公共模块的体积,只能是压缩和tree shaking

  1. 分包策略的其他配置

如果不想使用其他配置的默认值,可以手动进行配置:

  • automaticNameDelimiter:新chunk名称的分隔符,默认值~

  • minChunks:一个模块被多少个chunk使用时,才会进行分包,默认值1。如果我自己写一个文件,默认也不分包,因为自己写的那个太小,没达到拆分的条件,所以要配合minSize使用。

  • minSize:当分包达到多少字节后才允许被真正的拆分,默认值30000

  1. 缓存组

之前配置的分包策略是全局的

而实际上,分包策略是基于缓存组的

每个缓存组提供一套独有的策略,webpack按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包

默认情况下,webpack提供了两个缓存组:

js
module.exports = {
-  optimization:{
+    },
+  },
+};
+

事实上,分包策略有其默认的配置,我们只需要轻微的改动,即可应对大部分分包场景

chunks

该配置项用于配置需要应用分包策略的 chunk

我们知道,分包是从已有的 chunk 中分离出新的 chunk,那么哪些 chunk 需要分离呢

chunks 有三个取值,分别是:

  • all: 对于所有的 chunk 都要应用分包策略
  • async:【默认】仅针对异步 chunk 应用分包策略
  • initial:仅针对普通 chunk 应用分包策略

所以,你只需要配置 chunks 为 all 即可

maxSize

该配置可以控制包的最大字节数

如果某个包(包括分出来的包)超过了该值,则 webpack 会尽可能的将其分离成多个包

但是不要忽略的是,分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的,因此,尽管使用了这个配置,完全有可能某个包还是会超过这个体积

另外,该配置看上去很美妙,实际意义其实不大

因为分包的目的是提取大量的公共代码,从而减少总体积和充分利用浏览器缓存

虽然该配置可以把一些包进行再切分,但是实际的总体积和传输量并没有发生变化

如果要进一步减少公共模块的体积,只能是压缩和 tree shaking

  1. 分包策略的其他配置

如果不想使用其他配置的默认值,可以手动进行配置:

  • automaticNameDelimiter:新 chunk 名称的分隔符,默认值~

  • minChunks:一个模块被多少个 chunk 使用时,才会进行分包,默认值 1。如果我自己写一个文件,默认也不分包,因为自己写的那个太小,没达到拆分的条件,所以要配合 minSize 使用。

  • minSize:当分包达到多少字节后才允许被真正的拆分,默认值 30000

  1. 缓存组

之前配置的分包策略是全局的

而实际上,分包策略是基于缓存组的

每个缓存组提供一套独有的策略,webpack 按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包

默认情况下,webpack 提供了两个缓存组:

js
module.exports = {
+  optimization: {
     splitChunks: {
       //全局配置
       cacheGroups: {
         // 属性名是缓存组名称,会影响到分包的chunk名
         // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
-        vendors: { 
+        vendors: {
           test: /[\\\\/]node_modules[\\\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
-          priority: -10 // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
+          priority: -10, // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
         },
         default: {
-          minChunks: 2,  // 覆盖全局配置,将最小chunk引用数改为2
+          minChunks: 2, // 覆盖全局配置,将最小chunk引用数改为2
           priority: -20, // 优先级
-          reuseExistingChunk: true // 重用已经被分离出去的chunk
-        }
-      }
-    }
-  }
-}
+          reuseExistingChunk: true, // 重用已经被分离出去的chunk
+        },
+      },
+    },
+  },
+};
 
module.exports = {
-  optimization:{
+  optimization: {
     splitChunks: {
       //全局配置
       cacheGroups: {
         // 属性名是缓存组名称,会影响到分包的chunk名
         // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
-        vendors: { 
+        vendors: {
           test: /[\\\\/]node_modules[\\\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
-          priority: -10 // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
+          priority: -10, // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
         },
         default: {
-          minChunks: 2,  // 覆盖全局配置,将最小chunk引用数改为2
+          minChunks: 2, // 覆盖全局配置,将最小chunk引用数改为2
           priority: -20, // 优先级
-          reuseExistingChunk: true // 重用已经被分离出去的chunk
-        }
-      }
-    }
-  }
-}
+          reuseExistingChunk: true, // 重用已经被分离出去的chunk
+        },
+      },
+    },
+  },
+};
 

很多时候,缓存组对于我们来说没什么意义,因为默认的缓存组就已经够用了

但是我们同样可以利用缓存组来完成一些事情,比如对公共样式的抽离

js
module.exports = {
   optimization: {
     splitChunks: {
       chunks: "all",
       cacheGroups: {
-        styles: {// 样式抽离
+        styles: {
+          // 样式抽离
           test: /\\.css$/, // 匹配样式模块
           minSize: 0, // 覆盖默认的最小尺寸,这里仅仅是作为测试
-          minChunks: 2 // 覆盖默认的最小chunk引用数
-        }
-      }
-    }
+          minChunks: 2, // 覆盖默认的最小chunk引用数
+        },
+      },
+    },
   },
   module: {
-    rules: [{ test: /\\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }]
+    rules: [
+      { test: /\\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
+    ],
   },
   plugins: [
     new CleanWebpackPlugin(),
     new HtmlWebpackPlugin({
       template: "./public/index.html",
-      chunks: ["index"]
+      chunks: ["index"],
     }),
     new MiniCssExtractPlugin({
       filename: "[name].[hash:5].css",
       // chunkFilename是配置来自于分割chunk的文件名
-      chunkFilename: "common.[hash:5].css" 
-    })
-  ]
-}
+      chunkFilename: "common.[hash:5].css",
+    }),
+  ],
+};
 
module.exports = {
   optimization: {
     splitChunks: {
       chunks: "all",
       cacheGroups: {
-        styles: {// 样式抽离
+        styles: {
+          // 样式抽离
           test: /\\.css$/, // 匹配样式模块
           minSize: 0, // 覆盖默认的最小尺寸,这里仅仅是作为测试
-          minChunks: 2 // 覆盖默认的最小chunk引用数
-        }
-      }
-    }
+          minChunks: 2, // 覆盖默认的最小chunk引用数
+        },
+      },
+    },
   },
   module: {
-    rules: [{ test: /\\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }]
+    rules: [
+      { test: /\\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
+    ],
   },
   plugins: [
     new CleanWebpackPlugin(),
     new HtmlWebpackPlugin({
       template: "./public/index.html",
-      chunks: ["index"]
+      chunks: ["index"],
     }),
     new MiniCssExtractPlugin({
       filename: "[name].[hash:5].css",
       // chunkFilename是配置来自于分割chunk的文件名
-      chunkFilename: "common.[hash:5].css" 
-    })
-  ]
-}
-
  1. 配合多页应用

虽然现在单页应用是主流,但免不了还是会遇到多页应用

由于在多页应用中需要为每个html页面指定需要的chunk,否则都会引入进去,这就造成了问题

js
new HtmlWebpackPlugin({
+      chunkFilename: "common.[hash:5].css",
+    }),
+  ],
+};
+
  1. 配合多页应用

虽然现在单页应用是主流,但免不了还是会遇到多页应用

由于在多页应用中需要为每个 html 页面指定需要的 chunk,否则都会引入进去,这就造成了问题

js
new HtmlWebpackPlugin({
   template: "./public/index.html",
-  chunks: ["index~other", "vendors~index~other", "index"]
-})
+  chunks: ["index~other", "vendors~index~other", "index"],
+});
 
new HtmlWebpackPlugin({
   template: "./public/index.html",
-  chunks: ["index~other", "vendors~index~other", "index"]
-})
-

我们必须手动的指定被分离出去的chunk名称,这不是一种好办法

幸好html-webpack-plugin的新版本中解决了这一问题

shell
npm i -D html-webpack-plugin@next
+  chunks: ["index~other", "vendors~index~other", "index"],
+});
+

我们必须手动的指定被分离出去的 chunk 名称,这不是一种好办法

幸好html-webpack-plugin的新版本中解决了这一问题

shell
npm i -D html-webpack-plugin@next
 
npm i -D html-webpack-plugin@next
 

做出以下配置即可:

js
new HtmlWebpackPlugin({
   template: "./public/index.html",
-  chunks: ["index"]
-})
+  chunks: ["index"],
+});
 
new HtmlWebpackPlugin({
   template: "./public/index.html",
-  chunks: ["index"]
-})
-

它会自动的找到被index分离出去的chunk,并完成引用

目前这个版本仍处于测试解决,还未正式发布

  1. 原理

自动分包的原理其实并不复杂,主要经过以下步骤:

  • 检查每个chunk编译的结果
  • 根据分包策略,找到那些满足策略的模块
  • 根据分包策略,生成新的chunk打包这些模块(代码有所变化)
  • 把打包出去的模块从原始包中移除,并修正原始包代码

在代码层面,有以下变动

  1. 分包的代码中,加入一个全局变量webpackJsonp,类型为数组,其中包含公共模块的代码
  2. 原始包的代码中,使用数组中的公共代码

2.3 代码压缩

单模块体积优化

  1. 为什么要进行代码压缩: 减少代码体积;破坏代码的可读性,提升破解成本;
  2. 什么时候要进行代码压缩: 生产环境
  3. 使用什么压缩工具: 目前最流行的代码压缩工具主要有两个:UglifyJs和Terser

UglifyJs是一个传统的代码压缩工具,已存在多年,曾经是前端应用的必备工具,但由于它不支持ES6语法,所以目前的流行度已有所下降。

Terser是一个新起的代码压缩工具,支持ES6+语法,因此被很多构建工具内置使用。webpack安装后会内置Terser,当启用生产环境后即可用其进行代码压缩。

因此,我们选择Terser

关于副作用 side effect

副作用:函数运行过程中,可能会对外部环境造成影响的功能

如果函数中包含以下代码,该函数叫做副作用函数:

  • 异步代码
  • localStorage
  • 对外部数据的修改

如果一个函数没有副作用,同时,函数的返回结果仅依赖参数,则该函数叫做纯函数(pure function)

纯函数非常有利于压缩优化。可以手动指定那些是纯函数:pure_funcs:['Math.random']

Terser

在Terser的官网可尝试它的压缩效果

Terser官网:https://terser.org/

webpack+Terser

webpack自动集成了Terser

如果你想更改、添加压缩工具,又或者是想对Terser进行配置,使用下面的webpack配置即可

js
const TerserPlugin = require('terser-webpack-plugin');
-const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+  chunks: ["index"],
+});
+

它会自动的找到被 index 分离出去的 chunk,并完成引用

目前这个版本仍处于测试解决,还未正式发布

  1. 原理

自动分包的原理其实并不复杂,主要经过以下步骤:

  • 检查每个 chunk 编译的结果
  • 根据分包策略,找到那些满足策略的模块
  • 根据分包策略,生成新的 chunk 打包这些模块(代码有所变化)
  • 把打包出去的模块从原始包中移除,并修正原始包代码

在代码层面,有以下变动

  1. 分包的代码中,加入一个全局变量 webpackJsonp,类型为数组,其中包含公共模块的代码
  2. 原始包的代码中,使用数组中的公共代码

2.3 代码压缩

单模块体积优化

  1. 为什么要进行代码压缩: 减少代码体积;破坏代码的可读性,提升破解成本;
  2. 什么时候要进行代码压缩: 生产环境
  3. 使用什么压缩工具: 目前最流行的代码压缩工具主要有两个:UglifyJs 和 Terser

UglifyJs 是一个传统的代码压缩工具,已存在多年,曾经是前端应用的必备工具,但由于它不支持 ES6 语法,所以目前的流行度已有所下降。

Terser 是一个新起的代码压缩工具,支持 ES6+语法,因此被很多构建工具内置使用。webpack 安装后会内置 Terser,当启用生产环境后即可用其进行代码压缩。

因此,我们选择 Terser

关于副作用 side effect

副作用:函数运行过程中,可能会对外部环境造成影响的功能

如果函数中包含以下代码,该函数叫做副作用函数:

  • 异步代码
  • localStorage
  • 对外部数据的修改

如果一个函数没有副作用,同时,函数的返回结果仅依赖参数,则该函数叫做纯函数(pure function)

纯函数非常有利于压缩优化。可以手动指定那些是纯函数:pure_funcs:['Math.random']

Terser

在 Terser 的官网可尝试它的压缩效果

Terser 官网:https://terser.org/

webpack+Terser

webpack 自动集成了 Terser

如果你想更改、添加压缩工具,又或者是想对 Terser 进行配置,使用下面的 webpack 配置即可

js
const TerserPlugin = require("terser-webpack-plugin");
+const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
 module.exports = {
   optimization: {
     minimize: true, // 是否要启用压缩,默认情况下,生产环境会自动开启
-    minimizer: [ // 压缩时使用的插件,可以有多个
-      new TerserPlugin(), 
-      new OptimizeCSSAssetsPlugin()
+    minimizer: [
+      // 压缩时使用的插件,可以有多个
+      new TerserPlugin(),
+      new OptimizeCSSAssetsPlugin(),
     ],
   },
 };
-
const TerserPlugin = require('terser-webpack-plugin');
-const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+
const TerserPlugin = require("terser-webpack-plugin");
+const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
 module.exports = {
   optimization: {
     minimize: true, // 是否要启用压缩,默认情况下,生产环境会自动开启
-    minimizer: [ // 压缩时使用的插件,可以有多个
-      new TerserPlugin(), 
-      new OptimizeCSSAssetsPlugin()
+    minimizer: [
+      // 压缩时使用的插件,可以有多个
+      new TerserPlugin(),
+      new OptimizeCSSAssetsPlugin(),
     ],
   },
 };
-

2.4 tree shaking

压缩可以移除模块内部的无效代码 tree shaking 可以移除模块之间的无效代码

  1. 背景 某些模块导出的代码并不一定会被用到,第三方库就是个典型例子
js
// myMath.js
-export function add(a, b){
-  console.log("add")
-  return a+b;
+

2.4 tree shaking

压缩可以移除模块内部的无效代码 tree shaking 可以移除模块之间的无效代码

  1. 背景 某些模块导出的代码并不一定会被用到,第三方库就是个典型例子
js
// myMath.js
+export function add(a, b) {
+  console.log("add");
+  return a + b;
 }
 
-export function sub(a, b){
-  console.log("sub")
-  return a-b;
+export function sub(a, b) {
+  console.log("sub");
+  return a - b;
 }
 // index.js
-import {add} from "./myMath"
-console.log(add(1,2));
+import { add } from "./myMath";
+console.log(add(1, 2));
 
// myMath.js
-export function add(a, b){
-  console.log("add")
-  return a+b;
+export function add(a, b) {
+  console.log("add");
+  return a + b;
 }
 
-export function sub(a, b){
-  console.log("sub")
-  return a-b;
+export function sub(a, b) {
+  console.log("sub");
+  return a - b;
 }
 // index.js
-import {add} from "./myMath"
-console.log(add(1,2));
-

tree shaking 用于移除掉不会用到的导出

  1. 使用

webpack2开始就支持了tree shaking

只要是生产环境,tree shaking自动开启

  1. 原理

webpack会从入口模块出发寻找依赖关系

当解析一个模块时,webpack会根据ES6的模块导入语句来判断,该模块依赖了另一个模块的哪个导出

webpack之所以选择ES6的模块导入语句,是因为ES6模块有以下特点:commonjs不具备

  • 导入导出语句只能是顶层语句
  • import的模块名只能是字符串常量
  • import绑定的变量是不可变的

这些特征都非常有利于分析出稳定的依赖

在具体分析依赖时,webpack坚持的原则是:保证代码正常运行,然后再尽量tree shaking

所以,如果你依赖的是一个导出的对象,由于JS语言的动态特性,以及webpack还不够智能,为了保证代码正常运行,它不会移除对象中的任何信息

因此,我们在编写代码的时候,尽量:

  • 使用export xxx导出,而不使用export default {xxx}导出。后者会整个导出,但是不一定都需要。
  • 使用import {xxx} from "xxx"导入,而不使用import xxx from "xxx"导入

依赖分析完毕后,webpack会根据每个模块每个导出是否被使用,标记其他导出为dead code,然后交给代码压缩工具处理

代码压缩工具最终移除掉那些dead code代码

  1. 使用第三方库

某些第三方库可能使用的是commonjs的方式导出,比如lodash

又或者没有提供普通的ES6方式导出

对于这些库,tree shaking是无法发挥作用的

因此要寻找这些库的es6版本,好在很多流行但没有使用的ES6的第三方库,都发布了它的ES6版本,比如lodash-es

  1. 作用域分析

tree shaking本身并没有完善的作用域分析,可能导致在一些dead code函数中的依赖仍然会被视为依赖 比如a引用b,b引用了lodash,但是a没有用到b用lodash的导出代码 插件webpack-deep-scope-plugin提供了作用域分析,可解决这些问题

  1. 副作用问题

webpack在tree shaking的使用,有一个原则:一定要保证代码正确运行

在满足该原则的基础上,再来决定如何tree shaking

因此,当webpack无法确定某个模块是否有副作用时,它往往将其视为有副作用

因此,某些情况可能并不是我们所想要的

js
//common.js
-var n  = Math.random();
+import { add } from "./myMath";
+console.log(add(1, 2));
+

tree shaking 用于移除掉不会用到的导出

  1. 使用

webpack2 开始就支持了 tree shaking

只要是生产环境,tree shaking 自动开启

  1. 原理

webpack 会从入口模块出发寻找依赖关系

当解析一个模块时,webpack 会根据 ES6 的模块导入语句来判断,该模块依赖了另一个模块的哪个导出

webpack 之所以选择 ES6 的模块导入语句,是因为 ES6 模块有以下特点:commonjs 不具备

  • 导入导出语句只能是顶层语句
  • import 的模块名只能是字符串常量
  • import 绑定的变量是不可变的

这些特征都非常有利于分析出稳定的依赖

在具体分析依赖时,webpack 坚持的原则是:保证代码正常运行,然后再尽量 tree shaking

所以,如果你依赖的是一个导出的对象,由于 JS 语言的动态特性,以及 webpack 还不够智能,为了保证代码正常运行,它不会移除对象中的任何信息

因此,我们在编写代码的时候,尽量:

  • 使用 export xxx 导出,而不使用 export default {xxx}导出。后者会整个导出,但是不一定都需要。
  • 使用 import {xxx} from "xxx"导入,而不使用 import xxx from "xxx"导入

依赖分析完毕后,webpack 会根据每个模块每个导出是否被使用,标记其他导出为 dead code,然后交给代码压缩工具处理

代码压缩工具最终移除掉那些 dead code 代码

  1. 使用第三方库

某些第三方库可能使用的是 commonjs 的方式导出,比如 lodash

又或者没有提供普通的 ES6 方式导出

对于这些库,tree shaking 是无法发挥作用的

因此要寻找这些库的 es6 版本,好在很多流行但没有使用的 ES6 的第三方库,都发布了它的 ES6 版本,比如 lodash-es

  1. 作用域分析

tree shaking 本身并没有完善的作用域分析,可能导致在一些 dead code 函数中的依赖仍然会被视为依赖 比如 a 引用 b,b 引用了 lodash,但是 a 没有用到 b 用 lodash 的导出代码 插件 webpack-deep-scope-plugin 提供了作用域分析,可解决这些问题

  1. 副作用问题

webpack 在 tree shaking 的使用,有一个原则:一定要保证代码正确运行

在满足该原则的基础上,再来决定如何 tree shaking

因此,当 webpack 无法确定某个模块是否有副作用时,它往往将其视为有副作用

因此,某些情况可能并不是我们所想要的

js
//common.js
+var n = Math.random();
 
 //index.js
-import "./common.js"
+import "./common.js";
 
//common.js
-var n  = Math.random();
+var n = Math.random();
 
 //index.js
-import "./common.js"
-

虽然我们根本没用有common.js的导出,但webpack担心common.js有副作用,如果去掉会影响某些功能

如果要解决该问题,就需要标记该文件是没有副作用的

在package.json中加入sideEffects

json
{
-    "sideEffects": false
+import "./common.js";
+

虽然我们根本没用有 common.js 的导出,但 webpack 担心 common.js 有副作用,如果去掉会影响某些功能

如果要解决该问题,就需要标记该文件是没有副作用的

在 package.json 中加入 sideEffects

json
{
+  "sideEffects": false
 }
 
{
-    "sideEffects": false
+  "sideEffects": false
 }
-

有两种配置方式:

  • false:当前工程中,所有模块都没有副作用。注意,这种写法会影响到某些css文件的导入
  • 数组:设置哪些文件拥有副作用,例如:["!src/common.js"],表示只要不是src/common.js的文件,都有副作用
js
{
+

有两种配置方式:

  • false:当前工程中,所有模块都没有副作用。注意,这种写法会影响到某些 css 文件的导入
  • 数组:设置哪些文件拥有副作用,例如:["!src/common.js"],表示只要不是 src/common.js 的文件,都有副作用
js
{
     "sideEffects": ["!src/common.js"]
 }
 
{
     "sideEffects": ["!src/common.js"]
 }
-

这种方式我们一般不处理,通常是一些第三方库在它们自己的package.json中标注

webpack无法对css完成tree shaking,因为css跟es6没有半毛钱关系。

因此对css的tree shaking需要其他插件完成。例如:purgecss-webpack-plugin。注意:purgecss-webpack-plugin对css module无能为力

2.5 懒加载

可以理解为异步chunk

js
// 异步加载使用import语法
+

这种方式我们一般不处理,通常是一些第三方库在它们自己的 package.json 中标注

webpack 无法对 css 完成 tree shaking,因为 css 跟 es6 没有半毛钱关系。

因此对 css 的 tree shaking 需要其他插件完成。例如:purgecss-webpack-plugin。注意:purgecss-webpack-plugin 对 css module 无能为力

2.5 懒加载

可以理解为异步 chunk

js
// 异步加载使用import语法
 const btn = document.querySelector("button");
 btn.onclick = async function () {
   //动态加载
@@ -574,9 +596,7 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
   //浏览器会使用JSOP的方式远程去读取一个js模块
   //import()会返回一个promise   (返回结果类似于 * as obj)
   // const { chunk } = await import(/* webpackChunkName:"lodash" */"lodash-es");
-  const {
-    chunk
-  } = await import("./util");// 搞成静态依赖就行 所以加上了util.js
+  const { chunk } = await import("./util"); // 搞成静态依赖就行 所以加上了util.js
   const result = chunk([3, 5, 6, 7, 87], 2);
   console.log(result);
 };
@@ -591,39 +611,37 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
   //浏览器会使用JSOP的方式远程去读取一个js模块
   //import()会返回一个promise   (返回结果类似于 * as obj)
   // const { chunk } = await import(/* webpackChunkName:"lodash" */"lodash-es");
-  const {
-    chunk
-  } = await import("./util");// 搞成静态依赖就行 所以加上了util.js
+  const { chunk } = await import("./util"); // 搞成静态依赖就行 所以加上了util.js
   const result = chunk([3, 5, 6, 7, 87], 2);
   console.log(result);
 };
 
 // 因为是动态的,所以tree shaking没了
 // 如果想用该咋办?搞成静态依赖就行 所以加上了util.js
-

2.6 gzip

gzip是一种压缩文件的算法

B/S结构中的压缩传输

  • 浏览器告诉服务器支持那些压缩方式。
  • 响应头:什么方式解压->ungzip

优点:传输效率可能得到大幅提升

缺点:服务器的压缩需要时间,客户端的解压需要时间

gzip的原理

gizp压缩是一种http请求优化方式,通过减少文件体积来提高加载速度。html、js、css文件甚至json数据都可以用它压缩,可以减小60%以上的体积。前端配置gzip压缩,并且服务端使用nginx开启gzip,用来减小网络传输的流量大小。

使用webpack进行预压缩

使用compression-webpack-plugin插件对打包结果进行预压缩,可以移除服务器的压缩时间

js
plugins: [
+

2.6 gzip

gzip 是一种压缩文件的算法

B/S 结构中的压缩传输

  • 浏览器告诉服务器支持那些压缩方式。
  • 响应头:什么方式解压->ungzip

优点:传输效率可能得到大幅提升

缺点:服务器的压缩需要时间,客户端的解压需要时间

gzip 的原理

gizp 压缩是一种 http 请求优化方式,通过减少文件体积来提高加载速度。html、js、css 文件甚至 json 数据都可以用它压缩,可以减小 60%以上的体积。前端配置 gzip 压缩,并且服务端使用 nginx 开启 gzip,用来减小网络传输的流量大小。

使用 webpack 进行预压缩

使用 compression-webpack-plugin 插件对打包结果进行预压缩,可以移除服务器的压缩时间

js
plugins: [
   // 参考文档配置即可,一般取默认
   new CmpressionWebpackPlugin({
     test: /\\.js/, //希望对js进行预压缩
-    minRatio: 0.5 // 小于0.5才会压缩
-  })
-]
+    minRatio: 0.5, // 小于0.5才会压缩
+  }),
+];
 
plugins: [
   // 参考文档配置即可,一般取默认
   new CmpressionWebpackPlugin({
     test: /\\.js/, //希望对js进行预压缩
-    minRatio: 0.5 // 小于0.5才会压缩
-  })
-]
-

2.7 按需加载

在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 lodash 这种大型类库同样可以使用这个功能。

按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise,当 Promise 成功以后去执行回调。

2.8 其他

  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css
  • 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径
  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存
  • 提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码

提取第三方库 vendor: 这是也是 webpack 大法的 code splitting,提取一些第三方的库,从而减小 app.js 的大小。 代码层面做好懒加载,网络层面把 CDN、本地缓存用好,前端页面问题基本解决一大半了。剩下主要就是接口层面和“视觉上的快”的优化了,骨架屏先搞起,渲染一个“假页面”占位;接口该合并的合并,该拆分的拆分,如果是可滚动的长页面,就分批次请求

总结:如果有一个工程打包特别大-如何进行优化?

  1. CDN 如果工程中使用了一些知名的第三方库,可以考虑使用 CDN,而不进行打包
  2. 抽离公共模块 如果工程中用到了一些大的公共库,可以考虑将其分割出来单独打包
  3. 异步加载 对于那些不需要在一开始就执行的模块,可以考虑使用动态导入的方式异步加载它们,以尽量减少主包的体积
  4. 压缩、混淆
  5. tree shaking 尽量使用 ESM 语法进行导入导出,充分利用 tree shaking 去除无用代码
  6. gzip 开启 gzip 压缩,进一步减少包体积
  7. 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。

3. 运行性能

运行性能是指,JS代码在浏览器端的运行速度,它主要取决于我们如何书写高性能的代码

永远不要过早的关注于性能,因为你在开发的时候,无法完全预知最终的运行性能,过早的关注性能会极大的降低开发效率 性能优化主要从上面三个维度入手,性能优化没有完美的解决方案,需要具体情况具体分析

4. webpack5 内置优化

  1. webpack scope hoisting

scope hoisting 是 webpack 的内置优化,它是针对模块的优化,在生产环境打包时会自动开启。

在未开启scope hoisting时,webpack 会将每个模块的代码放置在一个独立的函数环境中,这样是为了保证模块的作用域互不干扰。

而 scope hoisting 的作用恰恰相反,是把多个模块的代码合并到一个函数环境中执行。在这一过程中,webpack 会按照顺序正确的合并模块代码,同时对涉及的标识符做适当处理以避免重名。

这样做的好处是减少了函数调用,对运行效率有一定提升,同时也降低了打包体积。

但 scope hoisting 的启用是有前提的,如果遇到某些模块多次被其他模块引用,或者使用了动态导入的模块,或者是非 ESM 的模块,都不会有 scope hoisting。

  1. 清除输出目录

webpack5清除输出目录开箱可用,无须安装clean-webpack-plugin,具体做法如下:

javascript
module.exports = {
+    minRatio: 0.5, // 小于0.5才会压缩
+  }),
+];
+

2.7 按需加载

在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 lodash 这种大型类库同样可以使用这个功能。

按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise,当 Promise 成功以后去执行回调。

2.8 其他

  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤ webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS ⽂件, 利⽤ cssnano (css-loader?minimize)来压缩 css
  • 利⽤ CDN 加速: 在构建过程中,将引⽤的静态资源路径修改为 CDN 上对应的路径。可以利⽤ webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径
  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存
  • 提取公共第三⽅库: SplitChunksPlugin 插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码

提取第三方库 vendor: 这是也是 webpack 大法的 code splitting,提取一些第三方的库,从而减小 app.js 的大小。 代码层面做好懒加载,网络层面把 CDN、本地缓存用好,前端页面问题基本解决一大半了。剩下主要就是接口层面和“视觉上的快”的优化了,骨架屏先搞起,渲染一个“假页面”占位;接口该合并的合并,该拆分的拆分,如果是可滚动的长页面,就分批次请求

总结:如果有一个工程打包特别大-如何进行优化?

  1. CDN 如果工程中使用了一些知名的第三方库,可以考虑使用 CDN,而不进行打包
  2. 抽离公共模块 如果工程中用到了一些大的公共库,可以考虑将其分割出来单独打包
  3. 异步加载 对于那些不需要在一开始就执行的模块,可以考虑使用动态导入的方式异步加载它们,以尽量减少主包的体积
  4. 压缩、混淆
  5. tree shaking 尽量使用 ESM 语法进行导入导出,充分利用 tree shaking 去除无用代码
  6. gzip 开启 gzip 压缩,进一步减少包体积
  7. 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。

3. 运行性能

运行性能是指,JS 代码在浏览器端的运行速度,它主要取决于我们如何书写高性能的代码

永远不要过早的关注于性能,因为你在开发的时候,无法完全预知最终的运行性能,过早的关注性能会极大的降低开发效率 性能优化主要从上面三个维度入手,性能优化没有完美的解决方案,需要具体情况具体分析

4. webpack5 内置优化

  1. webpack scope hoisting

scope hoisting 是 webpack 的内置优化,它是针对模块的优化,在生产环境打包时会自动开启。

在未开启 scope hoisting 时,webpack 会将每个模块的代码放置在一个独立的函数环境中,这样是为了保证模块的作用域互不干扰。

而 scope hoisting 的作用恰恰相反,是把多个模块的代码合并到一个函数环境中执行。在这一过程中,webpack 会按照顺序正确的合并模块代码,同时对涉及的标识符做适当处理以避免重名。

这样做的好处是减少了函数调用,对运行效率有一定提升,同时也降低了打包体积。

但 scope hoisting 的启用是有前提的,如果遇到某些模块多次被其他模块引用,或者使用了动态导入的模块,或者是非 ESM 的模块,都不会有 scope hoisting。

  1. 清除输出目录

webpack5清除输出目录开箱可用,无须安装clean-webpack-plugin,具体做法如下:

javascript
module.exports = {
   output: {
-    clean: true
-  }
-}
+    clean: true,
+  },
+};
 
module.exports = {
   output: {
-    clean: true
-  }
-}
+    clean: true,
+  },
+};
 
  1. top-level-await

webpack5现在允许在模块的顶级代码中直接使用await

javascript
// src/index.js
 const resp = await fetch("http://www.baidu.com");
 const jsonBody = await resp.json();
@@ -653,7 +671,6 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     index2: "./src/index2.js",
   },
 };
-
 
// webpack.config.js
 module.exports = {
   mode: "production",
@@ -663,90 +680,85 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     index2: "./src/index2.js",
   },
 };
-
-

  1. 打包缓存开箱即用

webpack4中,需要使用cache-loader缓存打包结果以优化之后的打包性能

而在webpack5中,默认就已经开启了打包缓存,无须再安装cache-loader

默认情况下,webpack5是将模块的打包结果缓存到内存中,可以通过cache配置进行更改

javascript
const path = require('path');
+

  1. 打包缓存开箱即用

webpack4中,需要使用cache-loader缓存打包结果以优化之后的打包性能

而在webpack5中,默认就已经开启了打包缓存,无须再安装cache-loader

默认情况下,webpack5是将模块的打包结果缓存到内存中,可以通过cache配置进行更改

javascript
const path = require("path");
 
 module.exports = {
-  mode: 'development',
-  devtool: 'source-map',
-  entry: './src/index.js',
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
   cache: {
-    type: 'filesystem', // 缓存类型,支持:memory、filesystem
-    cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'), // 缓存目录,仅类型为 filesystem 有效
+    type: "filesystem", // 缓存类型,支持:memory、filesystem
+    cacheDirectory: path.resolve(__dirname, "node_modules/.cache/webpack"), // 缓存目录,仅类型为 filesystem 有效
     // 更多配置参考:https://webpack.docschina.org/configuration/other-options/#cache
   },
 };
-
-
const path = require('path');
+
const path = require("path");
 
 module.exports = {
-  mode: 'development',
-  devtool: 'source-map',
-  entry: './src/index.js',
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
   cache: {
-    type: 'filesystem', // 缓存类型,支持:memory、filesystem
-    cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'), // 缓存目录,仅类型为 filesystem 有效
+    type: "filesystem", // 缓存类型,支持:memory、filesystem
+    cacheDirectory: path.resolve(__dirname, "node_modules/.cache/webpack"), // 缓存目录,仅类型为 filesystem 有效
     // 更多配置参考:https://webpack.docschina.org/configuration/other-options/#cache
   },
 };
+

关于cache的更多配置参考:https://webpack.docschina.org/configuration/other-options/#cache

  1. 资源模块

webpack4中,针对资源型文件我们通常使用file-loaderurl-loaderraw-loader进行处理

由于大部分前端项目都会用到资源型文件,因此webpack5原生支持了资源型模块

详见:https://webpack.docschina.org/guides/asset-modules/

javascript
// index.js
+import bigPic from "./assets/big-pic.png"; // 期望得到路径
+import smallPic from "./assets/small-pic.jpg"; // 期望得到base64
+import yueyunpeng from "./assets/yueyunpeng.gif"; // 期望根据文件大小决定是路径还是base64
+import raw from "./assets/raw.txt"; // 期望得到原始文件内容
 
-

关于cache的更多配置参考:https://webpack.docschina.org/configuration/other-options/#cache

  1. 资源模块

webpack4中,针对资源型文件我们通常使用file-loaderurl-loaderraw-loader进行处理

由于大部分前端项目都会用到资源型文件,因此webpack5原生支持了资源型模块

详见:https://webpack.docschina.org/guides/asset-modules/

javascript
// index.js
-import bigPic from './assets/big-pic.png'; // 期望得到路径
-import smallPic from './assets/small-pic.jpg'; // 期望得到base64
-import yueyunpeng from './assets/yueyunpeng.gif'; // 期望根据文件大小决定是路径还是base64
-import raw from './assets/raw.txt'; // 期望得到原始文件内容
-
-console.log('big-pic.png', bigPic);
-console.log('small-pic.jpg', smallPic);
-console.log('yueyunpeng.gif', yueyunpeng);
-console.log('raw.txt', raw);
-
+console.log("big-pic.png", bigPic);
+console.log("small-pic.jpg", smallPic);
+console.log("yueyunpeng.gif", yueyunpeng);
+console.log("raw.txt", raw);
 
// index.js
-import bigPic from './assets/big-pic.png'; // 期望得到路径
-import smallPic from './assets/small-pic.jpg'; // 期望得到base64
-import yueyunpeng from './assets/yueyunpeng.gif'; // 期望根据文件大小决定是路径还是base64
-import raw from './assets/raw.txt'; // 期望得到原始文件内容
-
-console.log('big-pic.png', bigPic);
-console.log('small-pic.jpg', smallPic);
-console.log('yueyunpeng.gif', yueyunpeng);
-console.log('raw.txt', raw);
+import bigPic from "./assets/big-pic.png"; // 期望得到路径
+import smallPic from "./assets/small-pic.jpg"; // 期望得到base64
+import yueyunpeng from "./assets/yueyunpeng.gif"; // 期望根据文件大小决定是路径还是base64
+import raw from "./assets/raw.txt"; // 期望得到原始文件内容
 
-
javascript
// webpack.config.js
-const path = require('path');
-const HtmlWebpackPlugin = require('html-webpack-plugin');
+console.log("big-pic.png", bigPic);
+console.log("small-pic.jpg", smallPic);
+console.log("yueyunpeng.gif", yueyunpeng);
+console.log("raw.txt", raw);
+
javascript
// webpack.config.js
+const path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
 module.exports = {
-  mode: 'development',
-  devtool: 'source-map',
-  entry: './src/index.js',
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
   devServer: {
     port: 8080,
   },
   plugins: [new HtmlWebpackPlugin()],
   output: {
-    filename: 'main.js',
-    path: path.resolve(__dirname, 'dist'),
-    assetModuleFilename: 'assets/[hash:5][ext]', // 在这里自定义资源文件保存的文件名
+    filename: "main.js",
+    path: path.resolve(__dirname, "dist"),
+    assetModuleFilename: "assets/[hash:5][ext]", // 在这里自定义资源文件保存的文件名
   },
   module: {
     rules: [
       {
         test: /\\.png/,
-        type: 'asset/resource', // 作用类似于 file-loader
+        type: "asset/resource", // 作用类似于 file-loader
       },
       {
         test: /\\.jpg/,
-        type: 'asset/inline', // 作用类似于 url-loader 文件大小不足的场景
+        type: "asset/inline", // 作用类似于 url-loader 文件大小不足的场景
       },
       {
         test: /\\.txt/,
-        type: 'asset/source', // 作用类似于 raw-loader
+        type: "asset/source", // 作用类似于 raw-loader
       },
       {
         test: /\\.gif/,
-        type: 'asset', // 作用类似于 url-loader。在导出一个 data uri 和发送一个单独的文件之间自动选择
+        type: "asset", // 作用类似于 url-loader。在导出一个 data uri 和发送一个单独的文件之间自动选择
         generator: {
-          filename: 'gif/[hash:5][ext]', // 这里的配置会覆盖 assetModuleFilename
+          filename: "gif/[hash:5][ext]", // 这里的配置会覆盖 assetModuleFilename
         },
         parser: {
           dataUrlCondition: {
@@ -758,42 +770,41 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     ],
   },
 };
-
 
// webpack.config.js
-const path = require('path');
-const HtmlWebpackPlugin = require('html-webpack-plugin');
+const path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
 module.exports = {
-  mode: 'development',
-  devtool: 'source-map',
-  entry: './src/index.js',
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
   devServer: {
     port: 8080,
   },
   plugins: [new HtmlWebpackPlugin()],
   output: {
-    filename: 'main.js',
-    path: path.resolve(__dirname, 'dist'),
-    assetModuleFilename: 'assets/[hash:5][ext]', // 在这里自定义资源文件保存的文件名
+    filename: "main.js",
+    path: path.resolve(__dirname, "dist"),
+    assetModuleFilename: "assets/[hash:5][ext]", // 在这里自定义资源文件保存的文件名
   },
   module: {
     rules: [
       {
         test: /\\.png/,
-        type: 'asset/resource', // 作用类似于 file-loader
+        type: "asset/resource", // 作用类似于 file-loader
       },
       {
         test: /\\.jpg/,
-        type: 'asset/inline', // 作用类似于 url-loader 文件大小不足的场景
+        type: "asset/inline", // 作用类似于 url-loader 文件大小不足的场景
       },
       {
         test: /\\.txt/,
-        type: 'asset/source', // 作用类似于 raw-loader
+        type: "asset/source", // 作用类似于 raw-loader
       },
       {
         test: /\\.gif/,
-        type: 'asset', // 作用类似于 url-loader。在导出一个 data uri 和发送一个单独的文件之间自动选择
+        type: "asset", // 作用类似于 url-loader。在导出一个 data uri 和发送一个单独的文件之间自动选择
         generator: {
-          filename: 'gif/[hash:5][ext]', // 这里的配置会覆盖 assetModuleFilename
+          filename: "gif/[hash:5][ext]", // 这里的配置会覆盖 assetModuleFilename
         },
         parser: {
           dataUrlCondition: {
@@ -805,30 +816,53 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     ],
   },
 };
-
-

字节跳动面试题:说一下项目里有做过哪些 webpack 上的优化

  1. 对传输性能的优化
  • 压缩和混淆 使用 Uglifyjs 或其他类似工具对打包结果进行压缩、混淆,可以有效的减少包体积
  • tree shaking 项目中尽量使用 ESM,可以有效利用 tree shaking 优化,降低包体积
  • 抽离公共模块 将一些公共代码单独打包,这样可以充分利用浏览器缓存,其他代码变动后,不影响公共代码,浏览器可以直接从缓存中找到公共代码。 具体方式有多种,比如 dll、splitChunks
  • 异步加载 对一些可以延迟执行的模块可以使用动态导入的方式异步加载它们,这样在打包结果中,它们会形成单独的包,同时,在页面一开始解析时并不需要加载它们,而是页面解析完成后,执行 JS 的过程中去加载它们。 这样可以显著提高页面的响应速度,在单页应用中尤其有用。
  • CDN 对一些知名的库使用 CDN,不仅可以节省打包时间,还可以显著提升库的加载速度
  • gzip 目前浏览器普遍支持 gzip 格式,因此可以将静态文件均使用 gzip 进行压缩
  • 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。
  1. 对打包过程的优化
  • noParse 很多第三方库本身就是已经打包好的代码,对于这种代码无须再进行解析,可以使用 noParse 配置排除掉这些第三方库
  • externals 对于一些知名的第三方库可以使用 CDN,这部分库可以通过 externals 配置不进行打包
  • 限制 loader 的范围 在使用 loader 的时候,可以通过 exclude 排除掉一些不必要的编译,比如 babel-loader 对于那些已经完成打包的第三方库没有必要再降级一次,可以排除掉
  • 开启 loader 缓存 可以利用cache-loader缓存 loader 的编译结果,避免在源码没有变动时反复编译
  • 开启多线程编译 可以利用thread-loader开启多线程编译,提升编译效率
  • 动态链接库 对于某些需要打包的第三方库,可以使用 dll 的方式单独对其打包,然后 DLLPlugin 将其整合到当前项目中,这样就避免了在开发中频繁去打包这些库
  1. 对开发体验的优化
  • lint 使用 eslint、stylelint 等工具保证团队代码风格一致
  • HMR 使用热替换避免页面刷新导致的状态丢失,提升开发体验

CSS

TIP

CSS 渲染性能优化

  1. 使用 id selector 非常的高效。在使用 id selector 的时候需要注意一点:因为 id 是唯一的,所以不需要既指定 id 又指定 tagName:
css
/* Bad  */
-p#id1 {color:red;}  
+

字节跳动面试题:说一下项目里有做过哪些 webpack 上的优化

  1. 对传输性能的优化
  • 压缩和混淆 使用 Uglifyjs 或其他类似工具对打包结果进行压缩、混淆,可以有效的减少包体积
  • tree shaking 项目中尽量使用 ESM,可以有效利用 tree shaking 优化,降低包体积
  • 抽离公共模块 将一些公共代码单独打包,这样可以充分利用浏览器缓存,其他代码变动后,不影响公共代码,浏览器可以直接从缓存中找到公共代码。 具体方式有多种,比如 dll、splitChunks
  • 异步加载 对一些可以延迟执行的模块可以使用动态导入的方式异步加载它们,这样在打包结果中,它们会形成单独的包,同时,在页面一开始解析时并不需要加载它们,而是页面解析完成后,执行 JS 的过程中去加载它们。 这样可以显著提高页面的响应速度,在单页应用中尤其有用。
  • CDN 对一些知名的库使用 CDN,不仅可以节省打包时间,还可以显著提升库的加载速度
  • gzip 目前浏览器普遍支持 gzip 格式,因此可以将静态文件均使用 gzip 进行压缩
  • 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。
  1. 对打包过程的优化
  • noParse 很多第三方库本身就是已经打包好的代码,对于这种代码无须再进行解析,可以使用 noParse 配置排除掉这些第三方库
  • externals 对于一些知名的第三方库可以使用 CDN,这部分库可以通过 externals 配置不进行打包
  • 限制 loader 的范围 在使用 loader 的时候,可以通过 exclude 排除掉一些不必要的编译,比如 babel-loader 对于那些已经完成打包的第三方库没有必要再降级一次,可以排除掉
  • 开启 loader 缓存 可以利用 cache-loader 缓存 loader 的编译结果,避免在源码没有变动时反复编译
  • 开启多线程编译 可以利用 thread-loader 开启多线程编译,提升编译效率
  • 动态链接库 对于某些需要打包的第三方库,可以使用 dll 的方式单独对其打包,然后 DLLPlugin 将其整合到当前项目中,这样就避免了在开发中频繁去打包这些库
  1. 对开发体验的优化
  • lint 使用 eslint、stylelint 等工具保证团队代码风格一致
  • HMR 使用热替换避免页面刷新导致的状态丢失,提升开发体验

CSS

TIP

CSS 渲染性能优化

  1. 使用 id selector 非常的高效。在使用 id selector 的时候需要注意一点:因为 id 是唯一的,所以不需要既指定 id 又指定 tagName:
css
/* Bad  */
+p#id1 {
+  color: red;
+}
 
 /* Good  */
-#id1 {color:red;}
+#id1 {
+  color: red;
+}
 
/* Bad  */
-p#id1 {color:red;}  
+p#id1 {
+  color: red;
+}
 
 /* Good  */
-#id1 {color:red;}
-
  1. 不要使用 attribute selector,如:p[att1=”val1”]。这样的匹配非常慢。更不要这样写:p[id="id1"]。这样将 id selector 退化成 attribute selector。
css
/* Bad  */
-p[id="jartto"]{color:red;}  
-p[class="blog"]{color:red;}  
+#id1 {
+  color: red;
+}
+
  1. 不要使用 attribute selector,如:p[att1=”val1”]。这样的匹配非常慢。更不要这样写:p[id="id1"]。这样将 id selector 退化成 attribute selector。
css
/* Bad  */
+p[id="jartto"] {
+  color: red;
+}
+p[class="blog"] {
+  color: red;
+}
 /* Good  */
-#jartto{color:red;}  
-.blog{color:red;}
+#jartto {
+  color: red;
+}
+.blog {
+  color: red;
+}
 
/* Bad  */
-p[id="jartto"]{color:red;}  
-p[class="blog"]{color:red;}  
+p[id="jartto"] {
+  color: red;
+}
+p[class="blog"] {
+  color: red;
+}
 /* Good  */
-#jartto{color:red;}  
-.blog{color:red;}
-
  1. 通常将浏览器前缀置于前面,将标准样式属性置于最后,类似:
css
.foo {
+#jartto {
+  color: red;
+}
+.blog {
+  color: red;
+}
+
  1. 通常将浏览器前缀置于前面,将标准样式属性置于最后,类似:
css
.foo {
   -moz-border-radius: 5px;
   border-radius: 5px;
 }
@@ -836,55 +870,55 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
   -moz-border-radius: 5px;
   border-radius: 5px;
 }
-

这里推荐参阅 CSS 规范-优化方案:http://nec.netease.com/standard/css-optimize.html

  1. 遵守 CSSLint 规则

font-faces         不能使用超过5个web字体

import            禁止使用@import

regex-selectors      禁止使用属性选择器中的正则表达式选择器

universal-selector       禁止使用通用选择器*

unqualified-attributes    禁止使用不规范的属性选择器

zero-units       0后面不要加单位

overqualified-elements    使用相邻选择器时,不要使用不必要的选择器

shorthand          简写样式属性

duplicate-background-images 相同的url在样式表中不超过一次

更多的 CSSLint 规则可以参阅:https://github.com/CSSLint/csslint

  1. 不要使用 @import

使用 @import 引入 CSS 会影响浏览器的并行下载。使用 @import 引用的 CSS 文件只有在引用它的那个 CSS 文件被下载、解析之后,浏览器才会知道还有另外一个 CSS 需要下载,这时才去下载,然后下载后开始解析、构建 Render Tree 等一系列操作。

多个 @import 会导致下载顺序紊乱。在 IE 中,@import 会引发资源文件的下载顺序被打乱,即排列在 @import 后面的 JS 文件先于 @import 下载,并且打乱甚至破坏 @import 自身的并行下载。

  1. 避免过分重排(Reflow)

所谓重排就是浏览器重新计算布局位置与大小。常见的重排元素:

width
-height 
-padding 
-margin 
-display 
-border-width 
-border 
-top 
-position 
-font-size 
-float 
-text-align 
-overflow-y 
-font-weight 
-overflow 
-left 
-font-family 
-line-height 
-vertical-align 
-right 
-clear 
-white-space 
-bottom 
+

这里推荐参阅 CSS 规范-优化方案:http://nec.netease.com/standard/css-optimize.html

  1. 遵守 CSSLint 规则

font-faces         不能使用超过 5 个 web 字体

import            禁止使用@import

regex-selectors      禁止使用属性选择器中的正则表达式选择器

universal-selector       禁止使用通用选择器*

unqualified-attributes    禁止使用不规范的属性选择器

zero-units        0 后面不要加单位

overqualified-elements    使用相邻选择器时,不要使用不必要的选择器

shorthand          简写样式属性

duplicate-background-images 相同的 url 在样式表中不超过一次

更多的 CSSLint 规则可以参阅:https://github.com/CSSLint/csslint

  1. 不要使用 @import

使用 @import 引入 CSS 会影响浏览器的并行下载。使用 @import 引用的 CSS 文件只有在引用它的那个 CSS 文件被下载、解析之后,浏览器才会知道还有另外一个 CSS 需要下载,这时才去下载,然后下载后开始解析、构建 Render Tree 等一系列操作。

多个 @import 会导致下载顺序紊乱。在 IE 中,@import 会引发资源文件的下载顺序被打乱,即排列在 @import 后面的 JS 文件先于 @import 下载,并且打乱甚至破坏 @import 自身的并行下载。

  1. 避免过分重排(Reflow)

所谓重排就是浏览器重新计算布局位置与大小。常见的重排元素:

width
+height
+padding
+margin
+display
+border-width
+border
+top
+position
+font-size
+float
+text-align
+overflow-y
+font-weight
+overflow
+left
+font-family
+line-height
+vertical-align
+right
+clear
+white-space
+bottom
 min-height
 
width
-height 
-padding 
-margin 
-display 
-border-width 
-border 
-top 
-position 
-font-size 
-float 
-text-align 
-overflow-y 
-font-weight 
-overflow 
-left 
-font-family 
-line-height 
-vertical-align 
-right 
-clear 
-white-space 
-bottom 
+height
+padding
+margin
+display
+border-width
+border
+top
+position
+font-size
+float
+text-align
+overflow-y
+font-weight
+overflow
+left
+font-family
+line-height
+vertical-align
+right
+clear
+white-space
+bottom
 min-height
-
  1. 依赖继承。如果某些属性可以继承,那么自然没有必要在写一遍。

  2. 其他:使用 id 选择器非常高效,因为 id 是唯一的;使用渐进增强的方案;值缩写;避免耗性能的属性;背景图优化合并;文件压缩

网络层面

总结

  • 优化打包体积:利用一些工具压缩、混淆最终打包代码,减少包体积
  • 多目标打包:利用一些打包插件,针对不同的浏览器打包出不同的兼容性版本,这样一来,每个版本中的兼容性代码就会大大减少,从而减少包体积
  • 压缩:现代浏览器普遍支持压缩格式,因此服务端的各种文件可以压缩后再响应给客户端,只要解压时间小于优化的传输时间,压缩就是可行的
  • CDN:利用 CDN 可以大幅缩减静态资源的访问时间,特别是对于公共库的访问,可以使用知名的 CDN 资源,这样可以实现跨越站点的缓存
  • 缓存:对于除 HTML 外的所有静态资源均可以开启协商缓存,利用构建工具打包产生的文件 hash 值来置换缓存
  • http2:开启 http2 后,利用其多路复用、头部压缩等特点,充分利用带宽传递大量的文件数据
  • 雪碧图:对于不使用 HTTP2 的场景,可以将多个图片合并为雪碧图,以达到减少文件的目的
  • defer、async:通过 defer 和 async 属性,可以让页面尽早加载 js 文件
  • prefetch、preload:通过 prefetch 属性,可以让页面在空闲时预先下载其他页面可能要用到的资源;通过 preload 属性,可以让页面预先下载本页面可能要用到的资源
  • 多个静态资源域:对于不使用 HTTP2 的场景,将相对独立的静态资源分到多个域中保存,可以让浏览器同时开启多个 TCP 连接,并行下载 (http2之前,浏览器开多个tcp,同一个域下最大数6个,为了多,静态资源分多个域存储,突破6个的限制)

CDN

CDN(Content Delivery Network,内容分发网络)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

典型的CDN系统构成:

  • 分发服务系统: 最基本的工作单元就是Cache设备,cache(边缘cache)负责直接响应最终用户的访问请求,把缓存在本地的内容快速地提供给用户。同时cache还负责与源站点进行内容同步,把更新的内容以及本地没有的内容从源站点获取并保存在本地。Cache设备的数量、规模、总服务能力是衡量一个CDN系统服务能力的最基本的指标。

  • 负载均衡系统: 主要功能是负责对所有发起服务请求的用户进行访问调度,确定提供给用户的最终实际访问地址。两级调度体系分为全局负载均衡(GSLB)和本地负载均衡(SLB)。全局负载均衡主要根据用户就近性原则,通过对每个服务节点进行“最优”判断,确定向用户提供服务的cache的物理位置。本地负载均衡主要负责节点内部的设备负载均衡

  • 运营管理系统: 运营管理系统分为运营管理和网络管理子系统,负责处理业务层面的与外界系统交互所必须的收集、整理、交付工作,包含客户管理、产品管理、计费管理、统计分析等功能。

作用

CDN一般会用来托管Web资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用CDN来加速这些资源的访问。

(1)在性能方面,引入CDN的作用在于:

  • 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快
  • 部分资源请求分配给了CDN,减少了服务器的负载

(2)在安全方面,CDN有助于防御DDoS、MITM等网络攻击:

  • 针对DDoS:通过监控分析异常流量,限制其请求频率
  • 针对MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信

除此之外,CDN作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰)等方面的优势。 CDN还会把文件最小化或者压缩文档的优化

原理

它的基本原理是:架设多台服务器,这些服务器定期从源站拿取资源保存本地,到让不同地域的用户能够通过访问最近的服务器获得资源

CDN和DNS有着密不可分的联系,先来看一下DNS的解析域名过程,在浏览器输入 www.test.com 的解析过程如下:

  1. 检查浏览器缓存
  2. 检查操作系统缓存,常见的如hosts文件
  3. 检查路由器缓存
  4. 如果前几步都没没找到,会向ISP(网络服务提供商)的LDNS服务器查询
  5. 如果LDNS服务器没找到,会向根域名服务器(Root Server)请求解析,分为以下几步:
  • 根服务器返回顶级域名(TLD)服务器如.com,.cn,.org等的地址,该例子中会返回.com的地址
  • 接着向顶级域名服务器发送请求,然后会返回次级域名(SLD)服务器的地址,本例子会返回.test的地址
  • 接着向次级域名服务器发送请求,然后会返回通过域名查询到的目标IP,本例子会返回www.test.com的地址
  • Local DNS Server会缓存结果,并返回给用户,缓存在系统中

CDN的工作原理:

  1. 用户未使用CDN缓存资源的过程:
  2. 浏览器通过DNS对域名进行解析(就是上面的DNS解析过程),依次得到此域名对应的IP地址
  3. 浏览器根据得到的IP地址,向域名的服务主机发送数据请求
  4. 服务器向浏览器返回响应数据

(2)用户使用CDN缓存资源的过程:

  1. 对于点击的数据的URL,经过本地DNS系统的解析,发现该URL对应的是一个CDN专用的DNS服务器,DNS系统就会将域名解析权交给CNAME指向的CDN专用的DNS服务器。
  2. CND专用DNS服务器将CND的全局负载均衡设备IP地址返回给用户
  3. 用户向CDN的全局负载均衡设备发起数据请求
  4. CDN的全局负载均衡设备根据用户的IP地址,以及用户请求的内容URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求
  5. 区域负载均衡设备选择一台合适的缓存服务器来提供服务,将该缓存服务器的IP地址返回给全局负载均衡设备
  6. 全局负载均衡设备把服务器的IP地址返回给用户
  7. 用户向该缓存服务器发起请求,缓存服务器响应用户的请求,将用户所需内容发送至用户终端。

如果缓存服务器没有用户想要的内容,那么缓存服务器就会向它的上一级缓存服务器请求内容,以此类推,直到获取到需要的资源。最后如果还是没有,就会回到自己的服务器去获取资源。

CNAME(意为:别名):在域名解析中,实际上解析出来的指定域名对应的IP地址,或者该域名的一个CNAME,然后再根据这个CNAME来查找对应的IP地址。

使用场景

  • 使用第三方的CDN服务:如果想要开源一些项目,可以使用第三方的CDN服务
  • 使用CDN进行静态资源的缓存:将自己网站的静态资源放在CDN上,比如js、css、图片等。可以将整个项目放在CDN上,完成一键部署。
  • 直播传送:直播本质上是使用流媒体进行传送,CDN也是支持流媒体传送的,所以直播完全可以使用CDN来提高访问速度。CDN在处理流媒体的时候与处理普通静态文件有所不同,普通文件如果在边缘节点没有找到的话,就会去上一层接着寻找,但是流媒体本身数据量就非常大,如果使用回源的方式,必然会带来性能问题,所以流媒体一般采用的都是主动推送的方式来进行。

TODO:cdn加速原理,没有缓存到哪里拿,CDN回源策略

分发的内容

静态内容:即使是静态内容也不是一直保存在cdn,源服务器发送文件给CDN的时候就可以利用HTTP头部的cache-control可以设置文件的缓存形式,cdn就知道哪些内容可以保存no-cache,那些不能no-store,那些保存多久max-age

动态内容:

工作流程:

静态内容:源服务器把静态内容提前备份给cdn(push),世界各地访问的时候就进的cdn服务器会把静态内容提供给用户,不需要每次劳烦源服务器。如果没有提前备份,cdn问源服务器要(pull),然后cdn备份,其他请球的用户可以马上拿到。

动态内容:源服务器很难做到提前预测到每个用户的动态内容提前给到cdn,如果等到用户索取动态内容cdn再向源服务器获取,这样cdn提供不了加速服务。但是有些是可以提供动态服务的:时间,有些cdn会提供可以运行在cdn上的接口,让源服务器用这些cdn接口,而不是源服务器自己的代码,用户就可以直接从cdn获取时间

问:cdn用什么方式来转移流量实现负载均衡?

和DNS域名解析根服务器的做法相似:任播通信:服务器对外都拥有同一个ip地址,如果收到了请求,请求就会由距离用户最近的服务器响应,任播技术把流量转移给没超载的服务器可以缓解。CDN还会用TLS/SSL证书对网站进行保护。 我们可以把项目中的所有静态资源都放到CDN上(收费),也可以利用现成免费的CDN获取公共库的资源

js
 
+
  1. 依赖继承。如果某些属性可以继承,那么自然没有必要在写一遍。

  2. 其他:使用 id 选择器非常高效,因为 id 是唯一的;使用渐进增强的方案;值缩写;避免耗性能的属性;背景图优化合并;文件压缩

网络层面

总结

  • 优化打包体积:利用一些工具压缩、混淆最终打包代码,减少包体积
  • 多目标打包:利用一些打包插件,针对不同的浏览器打包出不同的兼容性版本,这样一来,每个版本中的兼容性代码就会大大减少,从而减少包体积
  • 压缩:现代浏览器普遍支持压缩格式,因此服务端的各种文件可以压缩后再响应给客户端,只要解压时间小于优化的传输时间,压缩就是可行的
  • CDN:利用 CDN 可以大幅缩减静态资源的访问时间,特别是对于公共库的访问,可以使用知名的 CDN 资源,这样可以实现跨越站点的缓存
  • 缓存:对于除 HTML 外的所有静态资源均可以开启协商缓存,利用构建工具打包产生的文件 hash 值来置换缓存
  • http2:开启 http2 后,利用其多路复用、头部压缩等特点,充分利用带宽传递大量的文件数据
  • 雪碧图:对于不使用 HTTP2 的场景,可以将多个图片合并为雪碧图,以达到减少文件的目的
  • defer、async:通过 defer 和 async 属性,可以让页面尽早加载 js 文件
  • prefetch、preload:通过 prefetch 属性,可以让页面在空闲时预先下载其他页面可能要用到的资源;通过 preload 属性,可以让页面预先下载本页面可能要用到的资源
  • 多个静态资源域:对于不使用 HTTP2 的场景,将相对独立的静态资源分到多个域中保存,可以让浏览器同时开启多个 TCP 连接,并行下载 (http2 之前,浏览器开多个 tcp,同一个域下最大数 6 个,为了多,静态资源分多个域存储,突破 6 个的限制)

CDN

CDN(Content Delivery Network,内容分发网络)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

典型的 CDN 系统构成:

  • 分发服务系统: 最基本的工作单元就是 Cache 设备,cache(边缘 cache)负责直接响应最终用户的访问请求,把缓存在本地的内容快速地提供给用户。同时 cache 还负责与源站点进行内容同步,把更新的内容以及本地没有的内容从源站点获取并保存在本地。Cache 设备的数量、规模、总服务能力是衡量一个 CDN 系统服务能力的最基本的指标。

  • 负载均衡系统: 主要功能是负责对所有发起服务请求的用户进行访问调度,确定提供给用户的最终实际访问地址。两级调度体系分为全局负载均衡(GSLB)和本地负载均衡(SLB)。全局负载均衡主要根据用户就近性原则,通过对每个服务节点进行“最优”判断,确定向用户提供服务的 cache 的物理位置。本地负载均衡主要负责节点内部的设备负载均衡

  • 运营管理系统: 运营管理系统分为运营管理和网络管理子系统,负责处理业务层面的与外界系统交互所必须的收集、整理、交付工作,包含客户管理、产品管理、计费管理、统计分析等功能。

作用

CDN 一般会用来托管 Web 资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用 CDN 来加速这些资源的访问。

(1)在性能方面,引入 CDN 的作用在于:

  • 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快
  • 部分资源请求分配给了 CDN,减少了服务器的负载

(2)在安全方面,CDN 有助于防御 DDoS、MITM 等网络攻击:

  • 针对 DDoS:通过监控分析异常流量,限制其请求频率
  • 针对 MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信

除此之外,CDN 作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰)等方面的优势。 CDN 还会把文件最小化或者压缩文档的优化

原理

它的基本原理是:架设多台服务器,这些服务器定期从源站拿取资源保存本地,到让不同地域的用户能够通过访问最近的服务器获得资源

CDN 和 DNS 有着密不可分的联系,先来看一下 DNS 的解析域名过程,在浏览器输入 www.test.com 的解析过程如下:

  1. 检查浏览器缓存
  2. 检查操作系统缓存,常见的如 hosts 文件
  3. 检查路由器缓存
  4. 如果前几步都没没找到,会向 ISP(网络服务提供商)的 LDNS 服务器查询
  5. 如果 LDNS 服务器没找到,会向根域名服务器(Root Server)请求解析,分为以下几步:
  • 根服务器返回顶级域名(TLD)服务器如.com,.cn,.org 等的地址,该例子中会返回.com 的地址
  • 接着向顶级域名服务器发送请求,然后会返回次级域名(SLD)服务器的地址,本例子会返回.test 的地址
  • 接着向次级域名服务器发送请求,然后会返回通过域名查询到的目标 IP,本例子会返回www.test.com的地址
  • Local DNS Server 会缓存结果,并返回给用户,缓存在系统中

CDN 的工作原理:

  1. 用户未使用 CDN 缓存资源的过程:
  2. 浏览器通过 DNS 对域名进行解析(就是上面的 DNS 解析过程),依次得到此域名对应的 IP 地址
  3. 浏览器根据得到的 IP 地址,向域名的服务主机发送数据请求
  4. 服务器向浏览器返回响应数据

(2)用户使用 CDN 缓存资源的过程:

  1. 对于点击的数据的 URL,经过本地 DNS 系统的解析,发现该 URL 对应的是一个 CDN 专用的 DNS 服务器,DNS 系统就会将域名解析权交给 CNAME 指向的 CDN 专用的 DNS 服务器。
  2. CND 专用 DNS 服务器将 CND 的全局负载均衡设备 IP 地址返回给用户
  3. 用户向 CDN 的全局负载均衡设备发起数据请求
  4. CDN 的全局负载均衡设备根据用户的 IP 地址,以及用户请求的内容 URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求
  5. 区域负载均衡设备选择一台合适的缓存服务器来提供服务,将该缓存服务器的 IP 地址返回给全局负载均衡设备
  6. 全局负载均衡设备把服务器的 IP 地址返回给用户
  7. 用户向该缓存服务器发起请求,缓存服务器响应用户的请求,将用户所需内容发送至用户终端。

如果缓存服务器没有用户想要的内容,那么缓存服务器就会向它的上一级缓存服务器请求内容,以此类推,直到获取到需要的资源。最后如果还是没有,就会回到自己的服务器去获取资源。

CNAME(意为:别名):在域名解析中,实际上解析出来的指定域名对应的 IP 地址,或者该域名的一个 CNAME,然后再根据这个 CNAME 来查找对应的 IP 地址。

使用场景

  • 使用第三方的 CDN 服务:如果想要开源一些项目,可以使用第三方的 CDN 服务
  • 使用 CDN 进行静态资源的缓存:将自己网站的静态资源放在 CDN 上,比如 js、css、图片等。可以将整个项目放在 CDN 上,完成一键部署。
  • 直播传送:直播本质上是使用流媒体进行传送,CDN 也是支持流媒体传送的,所以直播完全可以使用 CDN 来提高访问速度。CDN 在处理流媒体的时候与处理普通静态文件有所不同,普通文件如果在边缘节点没有找到的话,就会去上一层接着寻找,但是流媒体本身数据量就非常大,如果使用回源的方式,必然会带来性能问题,所以流媒体一般采用的都是主动推送的方式来进行。

TODO:cdn 加速原理,没有缓存到哪里拿,CDN 回源策略

分发的内容

静态内容:即使是静态内容也不是一直保存在 cdn,源服务器发送文件给 CDN 的时候就可以利用 HTTP 头部的 cache-control 可以设置文件的缓存形式,cdn 就知道哪些内容可以保存 no-cache,那些不能 no-store,那些保存多久 max-age

动态内容:

工作流程:

静态内容:源服务器把静态内容提前备份给 cdn(push),世界各地访问的时候就进的 cdn 服务器会把静态内容提供给用户,不需要每次劳烦源服务器。如果没有提前备份,cdn 问源服务器要(pull),然后 cdn 备份,其他请球的用户可以马上拿到。

动态内容:源服务器很难做到提前预测到每个用户的动态内容提前给到 cdn,如果等到用户索取动态内容 cdn 再向源服务器获取,这样 cdn 提供不了加速服务。但是有些是可以提供动态服务的:时间,有些 cdn 会提供可以运行在 cdn 上的接口,让源服务器用这些 cdn 接口,而不是源服务器自己的代码,用户就可以直接从 cdn 获取时间

问:cdn 用什么方式来转移流量实现负载均衡?

和 DNS 域名解析根服务器的做法相似:任播通信:服务器对外都拥有同一个 ip 地址,如果收到了请求,请求就会由距离用户最近的服务器响应,任播技术把流量转移给没超载的服务器可以缓解。CDN 还会用 TLS/SSL 证书对网站进行保护。 我们可以把项目中的所有静态资源都放到 CDN 上(收费),也可以利用现成免费的 CDN 获取公共库的资源

js

 首先我们需要告诉webpack不要对公共库进行打包
 // vue.config.js
 module.exports = {
@@ -898,7 +932,7 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
 };
 然后在页面中手动加入cdn链接这里使用bootcn
 对于vuex和vue-router使用这种传统的方式引入的话会自动成为Vue的插件因此需要去掉Vue.use(xxx)
- 
+
 我们可以使用下面的代码来进行兼容
 // store.js
 import Vue from "vue";
@@ -917,7 +951,7 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
   // 没有使用传统的方式引入VueRouter
   Vue.use(VueRouter);
 }
-
 
+

 首先,我们需要告诉webpack不要对公共库进行打包
 // vue.config.js
 module.exports = {
@@ -931,7 +965,7 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
 };
 然后,在页面中手动加入cdn链接,这里使用bootcn
 对于vuex和vue-router,使用这种传统的方式引入的话会自动成为Vue的插件,因此需要去掉Vue.use(xxx)
- 
+
 我们可以使用下面的代码来进行兼容
 // store.js
 import Vue from "vue";
@@ -950,27 +984,29 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
   // 没有使用传统的方式引入VueRouter
   Vue.use(VueRouter);
 }
-

增加带宽

增加带宽可以提高资源的访问速度,从而提高首批的加载速度,我司项目带宽由 2M 升级到 5M,效果明显。

http内置优化

  • Http2
    • 头部压缩:专门的 HPACK 压缩算法
      • 索引表:客户端和服务器共同维护的一张表,表的内容分为 61 位的静态表(保存常用信息,例如:host/content-type)和动态表
      • 霍夫曼编码
  • 链路复用
    • Http1 建立起 Tcp 连接,发送请求之后,服务器在处理请求的等待期间,这个期间又没有数据去发送,称为空挡期。链接断开是在服务器响应回溯之后
      • keep-alive 链接保持一段时间
      • HTTP2 可以利用空档期
      • 不需要再重复建立链接
  • 二进制帧
    • Http1.1 文本字符分割的数据流,解析慢且容易出错
    • 二进制帧:帧长度、帧类型、帧标识 补充:采用 Http2 之后,可以减少资源合并的操作,因为首部压缩已经减少了多请求传输的数据量

数据传输层面

  • 缓存:浏览器缓存
    • 强缓存
      • cache-contorl: max-age=30
      • expires: Wed, 21 Oct 2021 07:28:00 GMT
  • 协商缓存
    • etag
    • last-modified
    • if-modified-since
    • if-none-match
  • 压缩
    • 数据压缩:gzip
    • 代码文件压缩:HTML/CSS/JS 中的注释、空格、长变量等
    • 静态资源:字体图标,去除元数据,缩小尺寸以及分辨率
    • 头与报文
      • http1.1 中减少不必要的头
      • 减少 cookie 数据量

Vue

Vue 开发优化

使用key

对于通过循环生成的列表,应给每个列表项一个稳定且唯一的key,这有利于在列表变动时,尽量少的删除、新增、改动元素

使用冻结的对象

冻结的对象不会被响应化

使用函数式组件

参见函数式组件

使用计算属性

如果模板中某个数据会使用多次,并且该数据是通过计算得到的,使用计算属性以缓存它们

非实时绑定的表单项

当使用v-model绑定一个表单项时,当用户改变表单项的状态时,也会随之改变数据,从而导致vue发生重渲染(rerender),这会带来一些性能的开销。

特别是当用户改变表单项时,页面有一些动画正在进行中,由于JS执行线程和浏览器渲染线程是互斥的,最终会导致动画出现卡顿。

我们可以通过使用lazy或不使用v-model的方式解决该问题,但要注意,这样可能会导致在某一个时间段内数据和表单项的值是不一致的。

保持对象引用稳定

在绝大部分情况下,vue触发rerender的时机是其依赖的数据发生变化

若数据没有发生变化,哪怕给数据重新赋值了,vue也是不会做出任何处理的

下面是vue判断数据没有变化的源码

js
// value 为旧值, newVal 为新值
-if (newVal === value || (newVal !== newVal && value !== value)) {//NaN
-  return
+

增加带宽

增加带宽可以提高资源的访问速度,从而提高首批的加载速度,我司项目带宽由 2M 升级到 5M,效果明显。

http 内置优化

  • Http2
    • 头部压缩:专门的 HPACK 压缩算法
      • 索引表:客户端和服务器共同维护的一张表,表的内容分为 61 位的静态表(保存常用信息,例如:host/content-type)和动态表
      • 霍夫曼编码
  • 链路复用
    • Http1 建立起 Tcp 连接,发送请求之后,服务器在处理请求的等待期间,这个期间又没有数据去发送,称为空挡期。链接断开是在服务器响应回溯之后
      • keep-alive 链接保持一段时间
      • HTTP2 可以利用空档期
      • 不需要再重复建立链接
  • 二进制帧
    • Http1.1 文本字符分割的数据流,解析慢且容易出错
    • 二进制帧:帧长度、帧类型、帧标识 补充:采用 Http2 之后,可以减少资源合并的操作,因为首部压缩已经减少了多请求传输的数据量

数据传输层面

  • 缓存:浏览器缓存
    • 强缓存
      • cache-contorl: max-age=30
      • expires: Wed, 21 Oct 2021 07:28:00 GMT
  • 协商缓存
    • etag
    • last-modified
    • if-modified-since
    • if-none-match
  • 压缩
    • 数据压缩:gzip
    • 代码文件压缩:HTML/CSS/JS 中的注释、空格、长变量等
    • 静态资源:字体图标,去除元数据,缩小尺寸以及分辨率
    • 头与报文
      • http1.1 中减少不必要的头
      • 减少 cookie 数据量

Vue

Vue 开发优化

使用 key

对于通过循环生成的列表,应给每个列表项一个稳定且唯一的 key,这有利于在列表变动时,尽量少的删除、新增、改动元素

使用冻结的对象

冻结的对象不会被响应化

使用函数式组件

参见函数式组件

使用计算属性

如果模板中某个数据会使用多次,并且该数据是通过计算得到的,使用计算属性以缓存它们

非实时绑定的表单项

当使用 v-model 绑定一个表单项时,当用户改变表单项的状态时,也会随之改变数据,从而导致 vue 发生重渲染(rerender),这会带来一些性能的开销。

特别是当用户改变表单项时,页面有一些动画正在进行中,由于 JS 执行线程和浏览器渲染线程是互斥的,最终会导致动画出现卡顿。

我们可以通过使用 lazy 或不使用 v-model 的方式解决该问题,但要注意,这样可能会导致在某一个时间段内数据和表单项的值是不一致的。

保持对象引用稳定

在绝大部分情况下,vue 触发 rerender 的时机是其依赖的数据发生变化

若数据没有发生变化,哪怕给数据重新赋值了,vue 也是不会做出任何处理的

下面是 vue 判断数据没有变化的源码

js
// value 为旧值, newVal 为新值
+if (newVal === value || (newVal !== newVal && value !== value)) {
+  //NaN
+  return;
 }
 
// value 为旧值, newVal 为新值
-if (newVal === value || (newVal !== newVal && value !== value)) {//NaN
-  return
+if (newVal === value || (newVal !== newVal && value !== value)) {
+  //NaN
+  return;
 }
-

因此,如果需要,只要能保证组件的依赖数据不发生变化,组件就不会重新渲染。

对于原始数据类型,保持其值不变即可

对于对象类型,保持其引用不变即可

从另一方面来说,由于可以通过保持属性引用稳定来避免子组件的重渲染,那么我们应该细分组件来尽量避免多余的渲染

使用v-show替代v-if

对于频繁切换显示状态的元素,使用v-show可以保证虚拟dom树的稳定,避免频繁的新增和删除元素,特别是对于那些内部包含大量dom元素的节点,这一点极其重要

关键字:频繁切换显示状态、内部包含大量dom元素

使用延迟装载(defer)

首页白屏时间主要受到两个因素的影响:

  • 打包体积过大 巨型包需要消耗大量的传输时间,导致JS传输完成前页面只有一个<div>,没有可显示的内容 <div id="app">好看的东西<div>

  • 需要立即渲染的内容太多 JS传输完成后,浏览器开始执行JS构造页面。 但可能一开始要渲染的组件太多,不仅JS执行的时间很长,而且执行完后浏览器要渲染的元素过多,从而导致页面白屏

打包体积过大需要自行优化打包体积,本节不予讨论

可以进行分包

本节仅讨论渲染内容太多的问题。

一个可行的办法就是延迟装载组件,让组件按照指定的先后顺序依次一个一个渲染出来

延迟装载是一个思路,本质上就是利用requestAnimationFrame事件分批渲染内容,它的具体实现多种多样

使用keep-alive

keep-alive组件是vue的内置组件,用于缓存内部组件实例。这样做的目的在于,keep-alive内部的组件切回时,不用重新创建组件实例,而直接使用缓存中的实例,一方面能够避免创建组件带来的开销,另一方面可以保留组件的状态(不仅是数据的保留,还要真实dom的保留)。

keep-alive具有include和exclude属性,通过它们可以控制哪些组件进入缓存。另外它还提供了max属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue会移除最久没有使用的组件缓存。

受keep-alive的影响,其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是activated和deactivated,它们分别在组件激活和失活时触发。第一次activated触发是在mounted之后

原理 在具体的实现上,keep-alive在内部维护了一个key数组和一个缓存对象

js
// keep-alive 内部的声明周期函数
+

因此,如果需要,只要能保证组件的依赖数据不发生变化,组件就不会重新渲染。

对于原始数据类型,保持其值不变即可

对于对象类型,保持其引用不变即可

从另一方面来说,由于可以通过保持属性引用稳定来避免子组件的重渲染,那么我们应该细分组件来尽量避免多余的渲染

使用 v-show 替代 v-if

对于频繁切换显示状态的元素,使用 v-show 可以保证虚拟 dom 树的稳定,避免频繁的新增和删除元素,特别是对于那些内部包含大量 dom 元素的节点,这一点极其重要

关键字:频繁切换显示状态、内部包含大量 dom 元素

使用延迟装载(defer)

首页白屏时间主要受到两个因素的影响:

  • 打包体积过大 巨型包需要消耗大量的传输时间,导致 JS 传输完成前页面只有一个<div>,没有可显示的内容 <div id="app">好看的东西<div>

  • 需要立即渲染的内容太多 JS 传输完成后,浏览器开始执行 JS 构造页面。 但可能一开始要渲染的组件太多,不仅 JS 执行的时间很长,而且执行完后浏览器要渲染的元素过多,从而导致页面白屏

打包体积过大需要自行优化打包体积,本节不予讨论

可以进行分包

本节仅讨论渲染内容太多的问题。

一个可行的办法就是延迟装载组件,让组件按照指定的先后顺序依次一个一个渲染出来

延迟装载是一个思路,本质上就是利用 requestAnimationFrame 事件分批渲染内容,它的具体实现多种多样

使用 keep-alive

keep-alive 组件是 vue 的内置组件,用于缓存内部组件实例。这样做的目的在于,keep-alive 内部的组件切回时,不用重新创建组件实例,而直接使用缓存中的实例,一方面能够避免创建组件带来的开销,另一方面可以保留组件的状态(不仅是数据的保留,还要真实 dom 的保留)。

keep-alive 具有 include 和 exclude 属性,通过它们可以控制哪些组件进入缓存。另外它还提供了 max 属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue 会移除最久没有使用的组件缓存。

受 keep-alive 的影响,其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是 activated 和 deactivated,它们分别在组件激活和失活时触发。第一次 activated 触发是在 mounted 之后

原理 在具体的实现上,keep-alive 在内部维护了一个 key 数组和一个缓存对象

js
// keep-alive 内部的声明周期函数
 created () {
   this.cache = Object.create(null)
   this.keys = []
 }
- 
+
 
// keep-alive 内部的声明周期函数
 created () {
   this.cache = Object.create(null)
   this.keys = []
 }
- 
-

key数组记录目前缓存的组件key值,如果组件没有指定key值,则会为其自动生成一个唯一的key值 cache对象以key值为键,vnode为值,用于缓存组件对应的虚拟DOM

在keep-alive的渲染函数中,其基本逻辑是判断当前渲染的vnode是否有对应的缓存,如果有,从缓存中读取到对应的组件实例;如果没有则将其缓存。 当缓存数量超过max数值时,keep-alive会移除掉key数组的第一个元素

js
render(){
+
+

key 数组记录目前缓存的组件 key 值,如果组件没有指定 key 值,则会为其自动生成一个唯一的 key 值 cache 对象以 key 值为键,vnode 为值,用于缓存组件对应的虚拟 DOM

在 keep-alive 的渲染函数中,其基本逻辑是判断当前渲染的 vnode 是否有对应的缓存,如果有,从缓存中读取到对应的组件实例;如果没有则将其缓存。 当缓存数量超过 max 数值时,keep-alive 会移除掉 key 数组的第一个元素

js
render(){
   const slot = this.$slots.default; // 获取默认插槽
   const vnode = getFirstComponentChild(slot); // 得到插槽中的第一个组件的vnode
   const name = getComponentName(vnode.componentOptions); //获取组件名字
@@ -982,7 +1018,7 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     vnode.componentInstance = cache[key].componentInstance
     remove(keys, key); // 删除key
     // 将key加入到数组末尾,这样是为了保证最近使用的组件在数组中靠后,反之靠前
-    keys.push(key); 
+    keys.push(key);
   } else {
     // 无缓存,进行缓存
     cache[key] = vnode
@@ -1006,7 +1042,7 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     vnode.componentInstance = cache[key].componentInstance
     remove(keys, key); // 删除key
     // 将key加入到数组末尾,这样是为了保证最近使用的组件在数组中靠后,反之靠前
-    keys.push(key); 
+    keys.push(key);
   } else {
     // 无缓存,进行缓存
     cache[key] = vnode
@@ -1018,23 +1054,23 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
   }
   return vnode;
 }
-

长列表优化

vue-virtual-scroller

首先这个库在使用上是很方便的,就是它提供了一个标签,相当于是对div标签的一个修改,可以实现列表的渲染等等功能

异步组件

在代码层面,vue组件本质上是一个配置对象

js
var comp = {
+

长列表优化

vue-virtual-scroller

首先这个库在使用上是很方便的,就是它提供了一个标签,相当于是对 div 标签的一个修改,可以实现列表的渲染等等功能

异步组件

在代码层面,vue 组件本质上是一个配置对象

js
var comp = {
   props: xxx,
   data: xxx,
   computed: xxx,
-  methods: xxx
-}
+  methods: xxx,
+};
 
var comp = {
   props: xxx,
   data: xxx,
   computed: xxx,
-  methods: xxx
-}
-

但有的时候,要得到某个组件配置对象需要一个异步的加载过程,比如:

  • 需要使用ajax获得某个数据之后才能加载该组件
  • 为了合理的分包,组件配置对象需要通过import(xxx)动态加载

如果一个组件需要通过异步的方式得到组件配置对象,该组件可以把它做成一个异步组件

js
/**
+  methods: xxx,
+};
+

但有的时候,要得到某个组件配置对象需要一个异步的加载过程,比如:

  • 需要使用 ajax 获得某个数据之后才能加载该组件
  • 为了合理的分包,组件配置对象需要通过 import(xxx)动态加载

如果一个组件需要通过异步的方式得到组件配置对象,该组件可以把它做成一个异步组件

js
/**
  * 异步组件本质上是一个函数
  * 该函数调用后返回一个Promise,Promise成功的结果是一个组件配置对象
  */
-const AsyncComponent = () => import("./MyComp")
+const AsyncComponent = () => import("./MyComp");
 
 var App = {
   components: {
@@ -1042,14 +1078,14 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
      * 你可以把该函数当做一个组件使用(异步组件)
      * Vue会调用该函数,并等待Promise完成,完成之前该组件位置什么也不渲染
      */
-    AsyncComponent 
-  }
-}
+    AsyncComponent,
+  },
+};
 
/**
  * 异步组件本质上是一个函数
  * 该函数调用后返回一个Promise,Promise成功的结果是一个组件配置对象
  */
-const AsyncComponent = () => import("./MyComp")
+const AsyncComponent = () => import("./MyComp");
 
 var App = {
   components: {
@@ -1057,26 +1093,32 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
      * 你可以把该函数当做一个组件使用(异步组件)
      * Vue会调用该函数,并等待Promise完成,完成之前该组件位置什么也不渲染
      */
-    AsyncComponent 
-  }
-}
-

异步组件的函数不仅可以返回一个Promise,还支持返回一个对象

应用:异步组件通常应用在路由懒加载中,以达到更好的分包;为了提高用户体验,可以在组件配置对象加载完成之前给用户显示一些提示信息

js
var routes = [
-  { path: "/", component: async () => {
-    console.log("组件开始加载"); 
-    const HomeComp = await import("./Views/Home.vue");
-    console.log("组件加载完毕");
-    return HomeComp;
-  } }
-]
+    AsyncComponent,
+  },
+};
+

异步组件的函数不仅可以返回一个 Promise,还支持返回一个对象

应用:异步组件通常应用在路由懒加载中,以达到更好的分包;为了提高用户体验,可以在组件配置对象加载完成之前给用户显示一些提示信息

js
var routes = [
+  {
+    path: "/",
+    component: async () => {
+      console.log("组件开始加载");
+      const HomeComp = await import("./Views/Home.vue");
+      console.log("组件加载完毕");
+      return HomeComp;
+    },
+  },
+];
 
var routes = [
-  { path: "/", component: async () => {
-    console.log("组件开始加载"); 
-    const HomeComp = await import("./Views/Home.vue");
-    console.log("组件加载完毕");
-    return HomeComp;
-  } }
-]
-

Vue3 内置优化

静态提升

下面的静态节点会被提升

  • 元素节点
  • 没有绑定动态内容
js

+  {
+    path: "/",
+    component: async () => {
+      console.log("组件开始加载");
+      const HomeComp = await import("./Views/Home.vue");
+      console.log("组件加载完毕");
+      return HomeComp;
+    },
+  },
+];
+

Vue3 内置优化

静态提升

下面的静态节点会被提升

  • 元素节点
  • 没有绑定动态内容
js

 // vue2 的静态节点
 render(){
   createVNode("h1", null, "Hello World")
@@ -1103,7 +1145,7 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
 

静态属性会被提升

js
<div class="user">
   {{user.name}}
 </div>
- 
+
 const hoisted = { class: "user" }
 
 function render(){
@@ -1113,7 +1155,7 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
 
<div class="user">
   {{user.name}}
 </div>
- 
+
 const hoisted = { class: "user" }
 
 function render(){
@@ -1152,10 +1194,14 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     <span>{{ user.name }}</span>
   </div>
 </div>
-

当编译器遇到大量连续(少量则不会,目前是至少20个连续节点)的静态内容,会直接将其编译为一个普通字符串节点

js
const _hoisted_2 = _createStaticVNode("<div class=\\"logo\\"><h1>logo</h1></div><ul class=\\"nav\\"><li><a href=\\"\\">menu</a></li><li><a href=\\"\\">menu</a></li><li><a href=\\"\\">menu</a></li><li><a href=\\"\\">menu</a></li><li><a href=\\"\\">menu</a></li></ul>")
-
const _hoisted_2 = _createStaticVNode("<div class=\\"logo\\"><h1>logo</h1></div><ul class=\\"nav\\"><li><a href=\\"\\">menu</a></li><li><a href=\\"\\">menu</a></li><li><a href=\\"\\">menu</a></li><li><a href=\\"\\">menu</a></li><li><a href=\\"\\">menu</a></li></ul>")
-

对ssr的作用非常明显

缓存事件处理函数

js
<button @click="count++">plus</button>
- 
+

当编译器遇到大量连续(少量则不会,目前是至少 20 个连续节点)的静态内容,会直接将其编译为一个普通字符串节点

js
const _hoisted_2 = _createStaticVNode(
+  '<div class="logo"><h1>logo</h1></div><ul class="nav"><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li></ul>'
+);
+
const _hoisted_2 = _createStaticVNode(
+  '<div class="logo"><h1>logo</h1></div><ul class="nav"><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li></ul>'
+);
+

对 ssr 的作用非常明显

缓存事件处理函数

js
<button @click="count++">plus</button>
+
 // vue2
 render(ctx){
   return createVNode("button", {
@@ -1171,9 +1217,9 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     onClick: cache[0] || (cache[0] = ($event) => (ctx.count++))//有的话,缓存;没有,count++
   })
 }
- 
+
 
<button @click="count++">plus</button>
- 
+
 // vue2
 render(ctx){
   return createVNode("button", {
@@ -1189,8 +1235,8 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     onClick: cache[0] || (cache[0] = ($event) => (ctx.count++))//有的话,缓存;没有,count++
   })
 }
- 
-

Block Tree

vue2在对比新旧树的时候,并不知道哪些节点是静态的,哪些是动态的,因此只能一层一层比较,这就浪费了大部分时间在比对静态节点上

Block节点记录了那些是动态的节点,对比的时候只对比动态节点

html
<form>
+
+

Block Tree

vue2 在对比新旧树的时候,并不知道哪些节点是静态的,哪些是动态的,因此只能一层一层比较,这就浪费了大部分时间在比对静态节点上

Block 节点记录了那些是动态的节点,对比的时候只对比动态节点

html
<form>
   <div>
     <label>账号:</label>
     <input v-model="user.loginId" />
@@ -1200,7 +1246,6 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     <input v-model="user.loginPwd" />
   </div>
 </form>
-
 
<form>
   <div>
     <label>账号:</label>
@@ -1211,14 +1256,13 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
     <input v-model="user.loginPwd" />
   </div>
 </form>
-
-

编译器 会把所有的动态节点标记,存到到根节点的数组中 ,到时候对比的时候只对比block动态节点。如果树不稳定,会有其他方案。

PatchFlag

依托于vue3强大的编译器。vue2在对比每一个节点时,并不知道这个节点哪些相关信息会发生变化,因此只能将所有信息依次比对

js
<div class="user" data-id="1" title="user name">
+

编译器 会把所有的动态节点标记,存到到根节点的数组中 ,到时候对比的时候只对比 block 动态节点。如果树不稳定,会有其他方案。

PatchFlag

依托于 vue3 强大的编译器。vue2 在对比每一个节点时,并不知道这个节点哪些相关信息会发生变化,因此只能将所有信息依次比对

js
<div class="user" data-id="1" title="user name">
   {{user.name}}
 </div>
 
<div class="user" data-id="1" title="user name">
   {{user.name}}
 </div>
-

标识:

  • 标识1:代表元素内容是动态的
  • 标识2:Class
  • 标识3:class+text

启用现代模式

为了兼容各种浏览器,vue-cli在内部使用了@babel/present-env对代码进行降级,你可以通过.browserlistrc配置来设置需要兼容的目标浏览器

这是一种比较偷懒的办法,因为对于那些使用现代浏览器的用户,它们也被迫使用了降级之后的代码,而降低的代码中包含了大量的polyfill,从而提升了包的体积

因此,我们希望提供两种打包结果:

  1. 降级后的包(大),提供给旧浏览器用户使用
  2. 未降级的包(小),提供给现代浏览器用户使用

除了应用webpack进行多次打包外,还可以利用vue-cli给我们提供的命令:

shell
vue-cli-service build --modern
+

标识:

  • 标识 1:代表元素内容是动态的
  • 标识 2:Class
  • 标识 3:class+text

启用现代模式

为了兼容各种浏览器,vue-cli 在内部使用了@babel/present-env 对代码进行降级,你可以通过.browserlistrc 配置来设置需要兼容的目标浏览器

这是一种比较偷懒的办法,因为对于那些使用现代浏览器的用户,它们也被迫使用了降级之后的代码,而降低的代码中包含了大量的 polyfill,从而提升了包的体积

因此,我们希望提供两种打包结果:

  1. 降级后的包(大),提供给旧浏览器用户使用
  2. 未降级的包(小),提供给现代浏览器用户使用

除了应用 webpack 进行多次打包外,还可以利用 vue-cli 给我们提供的命令:

shell
vue-cli-service build --modern
 
vue-cli-service build --modern
 

问题梳理

如何实现 vue 项目中的性能优化?

编码阶段

  • 尽量减少 data 中的数据,data 中的数据都会增加 gettersetter,会收集对应的 watcher
  • v-ifv-for 不能连用
  • 如果需要使用 v-for 给每项元素绑定事件时使用事件代理
  • SPA 页面采用 keep-alive 缓存组件
  • 在更多的情况下,使用 v-if 替代 v-show
  • key 保证唯一
  • 使用路由懒加载、异步组件
  • 防抖、节流
  • 第三方模块按需导入
  • 长列表滚动到可视区域动态加载
  • 图片懒加载

SEO 优化

  • 预渲染
  • 服务端渲染 SSR

打包优化

  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 使用 cdn 加载第三方模块
  • 多线程打包 happypack
  • splitChunks 抽离公共文件
  • sourceMap 优化

用户体验

  • 骨架屏
  • PWA

还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启 gzip 压缩等。

vue 中的 spa 应用如何优化首屏加载速度?

优化首屏加载可以从这几个方面开始:

  • 请求优化:CDN 将第三方的类库放到 CDN 上,能够大幅度减少生产环境中的项目体积,另外 CDN 能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。
  • 缓存:将长时间不会改变的第三方类库或者静态资源设置为强缓存,将 max-age 设置为一个非常长的时间,再将访问路径加上哈希达到哈希值变了以后保证获取到最新资源,好的缓存策略有助于减轻服务器的压力,并且显著的提升用户的体验
  • gzip:开启 gzip 压缩,通常开启 gzip 压缩能够有效的缩小传输资源的大小。
  • http2:如果系统首屏同一时间需要加载的静态资源非常多,但是浏览器对同域名的 tcp 连接数量是有限制的(chrome 为 6 个)超过规定数量的 tcp 连接,则必须要等到之前的请求收到响应后才能继续发送,而 http2 则可以在多个 tcp 连接中并发多个请求没有限制,在一些网络较差的环境开启 http2 性能提升尤为明显。
  • 懒加载:当 url 匹配到相应的路径时,通过 import 动态加载页面组件,这样首屏的代码量会大幅减少,webpack 会把动态加载的页面组件分离成单独的一个 chunk.js 文件
  • 预渲染:由于浏览器在渲染出页面之前,需要先加载和解析相应的 html、css 和 js 文件,为此会有一段白屏的时间,可以添加 loading,或者骨架屏幕尽可能的减少白屏对用户的影响体积优化
  • 合理使用第三方库:对于一些第三方 ui 框架、类库,尽量使用按需加载,减少打包体积
  • 使用可视化工具分析打包后的模块体积:webpack-bundle- analyzer 这个插件在每次打包后能够更加直观的分析打包后模块的体积,再对其中比较大的模块进行优化
  • 提高代码使用率:利用代码分割,将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程
  • 封装:构建良好的项目架构,按照项目需求就行全局组件,插件,过滤器,指令,utils 等做一 些公共封装,可以有效减少我们的代码量,而且更容易维护资源优化
  • 图片懒加载:使用图片懒加载可以优化同一时间减少 http 请求开销,避免显示图片导致的画面抖动,提高用户体验
  • 使用 svg 图标:相对于用一张图片来表示图标,svg 拥有更好的图片质量,体积更小,并且不需要开启额外的 http 请求
  • 压缩图片:可以使用 image-webpack-loader,在用户肉眼分辨不清的情况下一定程度上压缩图片

React

总结

shouldComponentUpdate 提供了两个参数 nextProps 和 nextState,表示下一次 props 和一次 state 的值,当函数返回 false 时候,render()方法不执行,组件也就不会渲染,返回 true 时,组件照常重渲染。此方法就是拿当前 props 中值和下一次 props 中的值进行对比,数据相等时,返回 false,反之返回 true。

需要注意,在进行新旧对比的时候,是**浅对比,**也就是说如果比较的数据时引用数据类型,只要数据的引用的地址没变,即使内容变了,也会被判定为 true。

面对这个问题,可以使用如下方法进行解决: (1)使用 setState 改变数据之前,先采用 ES6 中 assgin 进行拷贝,但是 assgin 只深拷贝的数据的第一层,所以说不是最完美的解决办法:

javascript
const o2 = Object.assign({}, this.state.obj);
 o2.student.count = "00000";
@@ -1240,68 +1284,68 @@ import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets
 this.setState({
   obj: o2,
 });
-

React 如何判断什么时候重新渲染组件?

组件状态的改变可以因为props的改变,或者直接通过setState方法改变。组件获得新的状态,然后 React 决定是否应该重新渲染组件。只要组件的 state 发生变化,React 就会对组件进行重新渲染。这是因为 React 中的shouldComponentUpdate方法默认返回true,这就是导致每次更新都重新渲染的原因。

当 React 将要渲染组件时会执行shouldComponentUpdate方法来看它是否返回true(组件应该更新,也就是重新渲染)。所以需要重写shouldComponentUpdate方法让它根据情况返回true或者false来告诉 React 什么时候重新渲染什么时候跳过重新渲染。

避免不必要的 render

React 基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。大多数情况下,React 对 DOM 的渲染效率足以业务日常。但在个别复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,其很重要的一个方向,就是避免不必要的渲染(Render)。这里提下优化的点:

  • shouldComponentUpdate 和 PureComponent

在 React 类组件中,可以利用 shouldComponentUpdate 或者 PureComponent 来减少因父组件更新而触发子组件的 render,从而达到目的。shouldComponentUpdate 来决定是否组件是否重新渲染,如果不希望组件重新渲染,返回 false 即可。

  • 利用高阶组件

在函数组件中,并没有 shouldComponentUpdate 这个生命周期,可以利用高阶组件,封装一个类似 PureComponet 的功能

  • 使用 React.memo

React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo 只能用于函数组件。

https://juejin.cn/post/6935584878071119885

高性能JavaScript

开发注意

遵循严格模式:"use strict"

将 JavaScript 本放在页面底部,加快渲染页面

将 JavaScript 脚本将脚本成组打包,减少请求

使用非阻塞方式下载 JavaScript 脚本

尽量使用局部变量来保存全局变量

尽量减少使用闭包

使用 window 对象属性方法时,省略 window

尽量减少对象成员嵌套

缓存 DOM 节点的访问

通过避免使用 eval() 和 Function() 构造器

给 setTimeout() 和 setInterval() 传递函数而不是字符串作为参数

尽量使用直接量创建对象和数组

最小化重绘 (repaint) 和回流 (reflow)

懒加载

懒加载的概念

懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。 如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。

懒加载的特点

  • 减少无用资源的加载:使用懒加载明显减少了服务器的压力和流量,同时也减小了浏览器的负担。
  • 提升用户体验: 如果同时加载较多图片,可能需要等待的时间较长,这样影响了用户体验,而使用懒加载就能大大的提高用户体验。
  • 防止加载过多图片而影响其他资源文件的加载 :会影响网站应用的正常使用。

懒加载的实现原理

图片的加载是由src引起的,当对src赋值时,浏览器就会请求图片资源。根据这个原理,我们使用HTML5 的data-xxx属性来储存图片的路径,在需要加载图片的时候,将data-xxx中图片的路径赋值给src,这样就实现了图片的按需加载,即懒加载。

注意:data-xxx 中的xxx可以自定义,这里我们使用data-src来定义。 懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可。

使用原生JavaScript实现懒加载

  1. IntersectionObserver api
  2. window.innerHeight 是浏览器可视区的高度
  3. document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离
  4. imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离)
  5. 图片加载条件:img.offsetTop < window.innerHeight + document.body.scrollTop;
html
<div class="container">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
+

React 如何判断什么时候重新渲染组件?

组件状态的改变可以因为props的改变,或者直接通过setState方法改变。组件获得新的状态,然后 React 决定是否应该重新渲染组件。只要组件的 state 发生变化,React 就会对组件进行重新渲染。这是因为 React 中的shouldComponentUpdate方法默认返回true,这就是导致每次更新都重新渲染的原因。

当 React 将要渲染组件时会执行shouldComponentUpdate方法来看它是否返回true(组件应该更新,也就是重新渲染)。所以需要重写shouldComponentUpdate方法让它根据情况返回true或者false来告诉 React 什么时候重新渲染什么时候跳过重新渲染。

避免不必要的 render

React 基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。大多数情况下,React 对 DOM 的渲染效率足以业务日常。但在个别复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,其很重要的一个方向,就是避免不必要的渲染(Render)。这里提下优化的点:

  • shouldComponentUpdate 和 PureComponent

在 React 类组件中,可以利用 shouldComponentUpdate 或者 PureComponent 来减少因父组件更新而触发子组件的 render,从而达到目的。shouldComponentUpdate 来决定是否组件是否重新渲染,如果不希望组件重新渲染,返回 false 即可。

  • 利用高阶组件

在函数组件中,并没有 shouldComponentUpdate 这个生命周期,可以利用高阶组件,封装一个类似 PureComponet 的功能

  • 使用 React.memo

React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo 只能用于函数组件。

https://juejin.cn/post/6935584878071119885

高性能 JavaScript

开发注意

遵循严格模式:"use strict"

将 JavaScript 本放在页面底部,加快渲染页面

将 JavaScript 脚本将脚本成组打包,减少请求

使用非阻塞方式下载 JavaScript 脚本

尽量使用局部变量来保存全局变量

尽量减少使用闭包

使用 window 对象属性方法时,省略 window

尽量减少对象成员嵌套

缓存 DOM 节点的访问

通过避免使用 eval() 和 Function() 构造器

给 setTimeout() 和 setInterval() 传递函数而不是字符串作为参数

尽量使用直接量创建对象和数组

最小化重绘 (repaint) 和回流 (reflow)

懒加载

懒加载的概念

懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。 如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。

懒加载的特点

  • 减少无用资源的加载:使用懒加载明显减少了服务器的压力和流量,同时也减小了浏览器的负担。
  • 提升用户体验: 如果同时加载较多图片,可能需要等待的时间较长,这样影响了用户体验,而使用懒加载就能大大的提高用户体验。
  • 防止加载过多图片而影响其他资源文件的加载 :会影响网站应用的正常使用。

懒加载的实现原理

图片的加载是由 src 引起的,当对 src 赋值时,浏览器就会请求图片资源。根据这个原理,我们使用 HTML5 的 data-xxx 属性来储存图片的路径,在需要加载图片的时候,将 data-xxx 中图片的路径赋值给 src,这样就实现了图片的按需加载,即懒加载。

注意:data-xxx 中的 xxx 可以自定义,这里我们使用 data-src 来定义。 懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可。

使用原生 JavaScript 实现懒加载

  1. IntersectionObserver api
  2. window.innerHeight 是浏览器可视区的高度
  3. document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离
  4. imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离)
  5. 图片加载条件:img.offsetTop < window.innerHeight + document.body.scrollTop;
html
<div class="container">
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
 </div>
 <script>
-  var imgs = document.querySelectorAll('img');
-  function lozyLoad(){
-    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
-    var winHeight= window.innerHeight;
-    for(var i=0;i < imgs.length;i++){
-      if(imgs[i].offsetTop < scrollTop + winHeight ){
-        imgs[i].src = imgs[i].getAttribute('data-src');
+  var imgs = document.querySelectorAll("img");
+  function lozyLoad() {
+    var scrollTop =
+      document.body.scrollTop || document.documentElement.scrollTop;
+    var winHeight = window.innerHeight;
+    for (var i = 0; i < imgs.length; i++) {
+      if (imgs[i].offsetTop < scrollTop + winHeight) {
+        imgs[i].src = imgs[i].getAttribute("data-src");
       }
     }
   }
   window.onscroll = lozyLoad();
 </script>
 
<div class="container">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
 </div>
 <script>
-  var imgs = document.querySelectorAll('img');
-  function lozyLoad(){
-    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
-    var winHeight= window.innerHeight;
-    for(var i=0;i < imgs.length;i++){
-      if(imgs[i].offsetTop < scrollTop + winHeight ){
-        imgs[i].src = imgs[i].getAttribute('data-src');
+  var imgs = document.querySelectorAll("img");
+  function lozyLoad() {
+    var scrollTop =
+      document.body.scrollTop || document.documentElement.scrollTop;
+    var winHeight = window.innerHeight;
+    for (var i = 0; i < imgs.length; i++) {
+      if (imgs[i].offsetTop < scrollTop + winHeight) {
+        imgs[i].src = imgs[i].getAttribute("data-src");
       }
     }
   }
   window.onscroll = lozyLoad();
 </script>
-

懒加载与预加载的区别

这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力

  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。
  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。 通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。

参考:https://juejin.cn/post/6844903455048335368#heading-5

回流与重绘

回流(重排)

当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流。 下面这些操作会导致回流:

  • 页面的首次渲染
  • 浏览器的窗口大小发生变化
  • 元素的内容发生变化
  • 元素的尺寸或者位置发生变化
  • 元素的字体大小发生变化
  • 激活CSS伪类
  • 查询某些属性或者调用某些方法
  • 添加或者删除可见的DOM元素、
  • 操作class属性
  • 设置style属性:.style....style...----->.class{}

在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的DOM元素重新排列,它的影响范围有两种:

  • 全局范围:从根节点开始,对整个渲染树进行重新布局
  • 局部范围:对渲染树的某部分或者一个渲染对象进行重新布局

重绘

当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。

下面这些操作会导致回流:

  • color、background 相关属性:background-color、background-image 等
  • outline 相关属性:outline-color、outline-width 、text-decoration
  • border-radius、visibility、box-shadow

注意: 当触发回流时,一定会触发重绘,但是重绘不一定会引发回流。

如何避免回流与重绘?

减少回流与重绘的措施:

  • 操作DOM时,尽量在低层级的DOM节点进行操作
  • 不要使用table布局, 一个小的改动可能会使整个table进行重新布局
  • 使用CSS的表达式
  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。
  • 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  • 避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中
  • 将元素先设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制。

浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列

浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。

上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。

如何优化动画?

对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作DOM,就就会导致页面的性能问题,我们可以将动画的position属性设置为absolute或者fixed,将动画脱离文档流,这样他的回流就不会影响到页面了。

CPU中央处理器,擅长逻辑运算

GPU显卡,擅长图片绘制,高精度的浮点数运算。{家用,专业},尽量少复杂动画,即少了GPU,烧性能。

在gpu层面上操作:改变opacity或者 transform:translate3d()/translatez();

最好添加translatez(0); 小hack告诉浏览器告诉浏览器另起一个层

css 1、使用transform 替代top 2、使用visibility 替换display:none ,因为前者只会引起重绘,后者会引发回流(改变了布局 3、避免使用table布局,可能很小的一个小改动会造成整个table的重新布局。 4、尽可能在DOM树的最末端改变class,回流是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点。 5、避免设置多层内联样式,CSS选择符从右往左匹配查找,避免节点层级过多。 CSS3硬件加速(GPU加速),使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。 js 1、避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。 2、避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。 3、避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。 4、对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

新方法:will-change:transform;专门处理GPU加速问题

应用:hover上去后才告诉浏览器要开启新层,点击才触发,总之提前一刻告诉就行

css
div{
+

懒加载与预加载的区别

这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力

  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。
  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。 通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。

参考:https://juejin.cn/post/6844903455048335368#heading-5

回流与重绘

回流(重排)

当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流。 下面这些操作会导致回流:

  • 页面的首次渲染
  • 浏览器的窗口大小发生变化
  • 元素的内容发生变化
  • 元素的尺寸或者位置发生变化
  • 元素的字体大小发生变化
  • 激活 CSS 伪类
  • 查询某些属性或者调用某些方法
  • 添加或者删除可见的 DOM 元素、
  • 操作 class 属性
  • 设置 style 属性:.style....style...----->.class{}

在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的 DOM 元素重新排列,它的影响范围有两种:

  • 全局范围:从根节点开始,对整个渲染树进行重新布局
  • 局部范围:对渲染树的某部分或者一个渲染对象进行重新布局

重绘

当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。

下面这些操作会导致回流:

  • color、background 相关属性:background-color、background-image 等
  • outline 相关属性:outline-color、outline-width 、text-decoration
  • border-radius、visibility、box-shadow

注意: 当触发回流时,一定会触发重绘,但是重绘不一定会引发回流。

如何避免回流与重绘?

减少回流与重绘的措施:

  • 操作 DOM 时,尽量在低层级的 DOM 节点进行操作
  • 不要使用 table 布局, 一个小的改动可能会使整个 table 进行重新布局
  • 使用 CSS 的表达式
  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。
  • 使用 absolute 或者 fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  • 避免频繁操作 DOM,可以创建一个文档片段 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中
  • 将元素先设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
  • 将 DOM 的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制。

浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列

浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。

上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。

如何优化动画?

对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作 DOM,就就会导致页面的性能问题,我们可以将动画的 position 属性设置为 absolute 或者 fixed,将动画脱离文档流,这样他的回流就不会影响到页面了。

CPU 中央处理器,擅长逻辑运算

GPU 显卡,擅长图片绘制,高精度的浮点数运算。{家用,专业},尽量少复杂动画,即少了 GPU,烧性能。

在 gpu 层面上操作:改变 opacity 或者 transform:translate3d()/translatez();

最好添加 translatez(0); 小 hack 告诉浏览器告诉浏览器另起一个层

css 1、使用 transform 替代 top 2、使用 visibility 替换 display:none ,因为前者只会引起重绘,后者会引发回流(改变了布局 3、避免使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。 4、尽可能在 DOM 树的最末端改变 class,回流是不可避免的,但可以减少其影响。尽可能在 DOM 树的最末端改变 class,可以限制了回流的范围,使其影响尽可能少的节点。 5、避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。 CSS3 硬件加速(GPU 加速),使用 css3 硬件加速,可以让 transform、opacity、filters 这些动画不会引起回流重绘。但是对于动画的其它属性,比如 background-color 这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。 js 1、避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。 2、避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。 3、避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。 4、对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

新方法:will-change:transform;专门处理 GPU 加速问题

应用:hover 上去后才告诉浏览器要开启新层,点击才触发,总之提前一刻告诉就行

css
div {
   width: 100px;
   height: 100px;
 }
-div.hover{
+div.hover {
   will-change: transform;
 }
-div.active{
+div.active {
   transform: scale(2, 3);
 }
-
-
div{
+
div {
   width: 100px;
   height: 100px;
 }
-div.hover{
+div.hover {
   will-change: transform;
 }
-div.active{
+div.active {
   transform: scale(2, 3);
 }
-
-

浏览器刷新页面的频率1s 60s

每16.7mm刷新一次

gpu 可以再一帧里渲染好页面,那么当你改动页面的元素或者实现动画的时候,将会非常流畅

documentFragment 是什么?用它跟直接操作 DOM 的区别是什么?

MDN中对documentFragment的解释:

DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。

当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的DOM操作时,我们就可以将DOM元素插入DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作DOM相比,将DocumentFragment 节点插入DOM树时,不会触发页面的重绘,这样就大大提高了页面的性能。

防抖函数

  • 按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次
  • 服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤lodash.debounce 节流函数的适⽤场景:
  • 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动
  • 缩放场景:监控浏览器resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

如何对项目中的图片进行优化?

  1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
  2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
  3. 小图使用 base64 格式
  4. 将多个图标文件整合到一张图片中(雪碧图)
  5. 选择正确的图片格式:
  • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
  • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
  • 照片使用 JPEG

常见的图片格式及使用场景

(1)BMP,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以BMP格式的图片通常是较大的文件。

(2)GIF是无损的、采用索引色的点阵图。采用LZW压缩算法进行编码。文件小,是GIF格式的优点,同时,GIF格式还具有支持动画以及透明的优点。但是GIF格式仅支持8bit的索引色,所以GIF格式适用于对色彩要求不高同时需要文件体积较小的场景。

(3)JPEG是有损的、采用直接色的点阵图。JPEG的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG非常适合用来存储照片,与GIF相比,JPEG不适合用来存储企业Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较GIF更大。

(4)PNG-8是无损的、使用索引色的点阵图。PNG是一种比较新的图片格式,PNG-8是非常好的GIF格式替代者,在可能的情况下,应该尽可能的使用PNG-8而不是GIF,因为在相同的图片效果下,PNG-8具有更小的文件体积。除此之外,PNG-8还支持透明度的调节,而GIF并不支持。除非需要动画的支持,否则没有理由使用GIF而不是PNG-8。

(5)PNG-24是无损的、使用直接色的点阵图。PNG-24的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24格式的文件大小要比BMP小得多。当然,PNG24的图片还是要比JPEG、GIF、PNG-8大得多。

(6)SVG是无损的矢量图。SVG是矢量图意味着SVG图片由直线和曲线以及绘制它们的方法组成。当放大SVG图片时,看到的还是线和曲线,而不会出现像素点。这意味着SVG图片在放大时,不会失真,所以它非常适合用来绘制Logo、Icon等。

(7)WebP是谷歌开发的一种新图片格式,WebP是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为Web而生的,什么叫为Web而生呢?就是说相同质量的图片,WebP具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有Chrome浏览器和Opera浏览器支持WebP格式,兼容性不太好。WebP图片格式支持图片透明度,一个无损压缩的WebP图片,如果要支持透明度只需要22%的格外文件大小。

优化首屏响应

觉得快

loading

vue页面需要通过js构建,因此在js下载到本地之前,页面上什么也没有 一个非常简单有效的办法,即在页面中先渲染一个小的加载中效果,等到js下载到本地并运行后,即会自动替换

nprogress

源码分析地址:https://blog.csdn.net/qq_31968791/article/details/106790179 使用到的库是什么 nprogress 进度条的实现原理知道吗 Nprogress的原理非常简单,就是页面启动的时候,构建一个方法,创建一个div,然后这个div靠近最顶部,用fixed定位住,至于样式就是按照自个或者默认走了。 怎么使用这个库的 主要采用的两个方法是nprogress.start和nprogress.done 如何使用: 在请求拦截器中调用nprogress.start 在响应拦截器中调用nprogress.done

betterScroll

https://blog.csdn.net/weixin_37719279/article/details/82084342 使用的库是什么:better-scroll

骨架屏

骨架屏的原理:https://blog.csdn.net/csdn_yudong/article/details/103909178

你能说说为啥使用骨架屏吗?

现在的前端开发领域,都是前后端分离,前端框架主流的都是 SPA,MPA;这就意味着,页面渲染以及等待的白屏时间,成为我们需要解决的问题点;而且大项目,这个问题尤为突出。 webpack 可以实现按需加载,减小我们首屏需要加载的代码体积;再配合上 CDN 以及一些静态代码(框架,组件库等等…)缓存技术,可以很好的缓解这个加载渲染的时间过长的问题。 但即便如此,首屏的加载依然还是存在这个加载以及渲染的等待时间问题; 现在的前端开发领域,都是前后端分离,前端框架主流的都是 SPA,MPA;这就意味着,页面渲染以及等待的白屏时间,成为我们需要解决的问题点;而且大项目,这个问题尤为突出。 webpack 可以实现按需加载,减小我们首屏需要加载的代码体积;再配合上 CDN 以及一些静态代码(框架,组件库等等…)缓存技术,可以很好的缓解这个加载渲染的时间过长的问题。 目前主流,常见的解决方案是使用骨架屏技术,包括很多原生的APP,在页面渲染时,也会使用骨架屏。(下图中,红圈中的部分,即为骨架屏在内容还没有出现之前的页面骨架填充,以免留白)

骨架屏的要怎么使用呢?骨架屏的原理知道吗?

  1. 在 index.html 中的 div#app 中来实现骨架屏,程序渲染后就会替换掉 index.html 里面的 div#app 骨架屏内容;
  2. 使用一个Base64的图片来作为骨架屏

使用图片作为骨架屏; 简单暴力,让UI同学花点功夫吧;小米商城的移动端页面采用的就是这个方法,它是使用了一个Base64的图片来作为骨架屏。 按照方案一的方案,将这个 Base64 的图片写在我们的 index.html 模块中的 div#app 里面。

  1. 使用 .vue 文件来完成骨架屏

真实快

webpack 怎么进行首屏加载的优化?

  1. CDN 如果工程中使用了一些知名的第三方库,可以考虑使用 CDN,而不进行打包
  2. 抽离公共模块 如果工程中用到了一些大的公共库,可以考虑将其分割出来单独打包
  3. 异步加载 对于那些不需要在一开始就执行的模块,可以考虑使用动态导入的方式异步加载它们,以尽量减少主包的体积
  4. 压缩、混淆
  5. tree shaking 尽量使用 ESM 语法进行导入导出,充分利用 tree shaking 去除无用代码
  6. gzip 开启 gzip 压缩,进一步减少包体积
  7. 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。
`,592),E=[g];function k(v,f,q,D,w,x){return n(),a("div",null,E)}const P=s(h,[["render",k]]);export{_ as __pageData,P as default}; +

浏览器刷新页面的频率 1s 60s

每 16.7mm 刷新一次

gpu 可以再一帧里渲染好页面,那么当你改动页面的元素或者实现动画的时候,将会非常流畅

documentFragment 是什么?用它跟直接操作 DOM 的区别是什么?

MDN 中对 documentFragment 的解释:

DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document 使用,就像标准的 document 一样,存储由节点(nodes)组成的文档结构。与 document 相比,最大的区别是 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。

当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的 DOM 操作时,我们就可以将 DOM 元素插入 DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作 DOM 相比,将 DocumentFragment 节点插入 DOM 树时,不会触发页面的重绘,这样就大大提高了页面的性能。

防抖函数

如何对项目中的图片进行优化?

  1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
  2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
  3. 小图使用 base64 格式
  4. 将多个图标文件整合到一张图片中(雪碧图)
  5. 选择正确的图片格式:

常见的图片格式及使用场景

(1)BMP,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以 BMP 格式的图片通常是较大的文件。

(2)GIF 是无损的、采用索引色的点阵图。采用 LZW 压缩算法进行编码。文件小,是 GIF 格式的优点,同时,GIF 格式还具有支持动画以及透明的优点。但是 GIF 格式仅支持 8bit 的索引色,所以 GIF 格式适用于对色彩要求不高同时需要文件体积较小的场景。

(3)JPEG 是有损的、采用直接色的点阵图。JPEG 的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG 非常适合用来存储照片,与 GIF 相比,JPEG 不适合用来存储企业 Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较 GIF 更大。

(4)PNG-8 是无损的、使用索引色的点阵图。PNG 是一种比较新的图片格式,PNG-8 是非常好的 GIF 格式替代者,在可能的情况下,应该尽可能的使用 PNG-8 而不是 GIF,因为在相同的图片效果下,PNG-8 具有更小的文件体积。除此之外,PNG-8 还支持透明度的调节,而 GIF 并不支持。除非需要动画的支持,否则没有理由使用 GIF 而不是 PNG-8。

(5)PNG-24 是无损的、使用直接色的点阵图。PNG-24 的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24 格式的文件大小要比 BMP 小得多。当然,PNG24 的图片还是要比 JPEG、GIF、PNG-8 大得多。

(6)SVG 是无损的矢量图。SVG 是矢量图意味着 SVG 图片由直线和曲线以及绘制它们的方法组成。当放大 SVG 图片时,看到的还是线和曲线,而不会出现像素点。这意味着 SVG 图片在放大时,不会失真,所以它非常适合用来绘制 Logo、Icon 等。

(7)WebP 是谷歌开发的一种新图片格式,WebP 是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为 Web 而生的,什么叫为 Web 而生呢?就是说相同质量的图片,WebP 具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有 Chrome 浏览器和 Opera 浏览器支持 WebP 格式,兼容性不太好。WebP 图片格式支持图片透明度,一个无损压缩的 WebP 图片,如果要支持透明度只需要 22%的格外文件大小。

优化首屏响应

觉得快

loading

vue 页面需要通过 js 构建,因此在 js 下载到本地之前,页面上什么也没有 一个非常简单有效的办法,即在页面中先渲染一个小的加载中效果,等到 js 下载到本地并运行后,即会自动替换

nprogress

源码分析地址:https://blog.csdn.net/qq_31968791/article/details/106790179 使用到的库是什么 nprogress 进度条的实现原理知道吗 Nprogress 的原理非常简单,就是页面启动的时候,构建一个方法,创建一个 div,然后这个 div 靠近最顶部,用 fixed 定位住,至于样式就是按照自个或者默认走了。 怎么使用这个库的 主要采用的两个方法是 nprogress.start 和 nprogress.done 如何使用: 在请求拦截器中调用 nprogress.start 在响应拦截器中调用 nprogress.done

betterScroll

https://blog.csdn.net/weixin_37719279/article/details/82084342 使用的库是什么:better-scroll

骨架屏

骨架屏的原理:https://blog.csdn.net/csdn_yudong/article/details/103909178

你能说说为啥使用骨架屏吗?

现在的前端开发领域,都是前后端分离,前端框架主流的都是 SPA,MPA;这就意味着,页面渲染以及等待的白屏时间,成为我们需要解决的问题点;而且大项目,这个问题尤为突出。 webpack 可以实现按需加载,减小我们首屏需要加载的代码体积;再配合上 CDN 以及一些静态代码(框架,组件库等等…)缓存技术,可以很好的缓解这个加载渲染的时间过长的问题。 但即便如此,首屏的加载依然还是存在这个加载以及渲染的等待时间问题; 现在的前端开发领域,都是前后端分离,前端框架主流的都是 SPA,MPA;这就意味着,页面渲染以及等待的白屏时间,成为我们需要解决的问题点;而且大项目,这个问题尤为突出。 webpack 可以实现按需加载,减小我们首屏需要加载的代码体积;再配合上 CDN 以及一些静态代码(框架,组件库等等…)缓存技术,可以很好的缓解这个加载渲染的时间过长的问题。 目前主流,常见的解决方案是使用骨架屏技术,包括很多原生的 APP,在页面渲染时,也会使用骨架屏。(下图中,红圈中的部分,即为骨架屏在内容还没有出现之前的页面骨架填充,以免留白)

骨架屏的要怎么使用呢?骨架屏的原理知道吗?

  1. 在 index.html 中的 div#app 中来实现骨架屏,程序渲染后就会替换掉 index.html 里面的 div#app 骨架屏内容;
  2. 使用一个 Base64 的图片来作为骨架屏

使用图片作为骨架屏; 简单暴力,让 UI 同学花点功夫吧;小米商城的移动端页面采用的就是这个方法,它是使用了一个 Base64 的图片来作为骨架屏。 按照方案一的方案,将这个 Base64 的图片写在我们的 index.html 模块中的 div#app 里面。

  1. 使用 .vue 文件来完成骨架屏

真实快

webpack 怎么进行首屏加载的优化?

  1. CDN 如果工程中使用了一些知名的第三方库,可以考虑使用 CDN,而不进行打包
  2. 抽离公共模块 如果工程中用到了一些大的公共库,可以考虑将其分割出来单独打包
  3. 异步加载 对于那些不需要在一开始就执行的模块,可以考虑使用动态导入的方式异步加载它们,以尽量减少主包的体积
  4. 压缩、混淆
  5. tree shaking 尽量使用 ESM 语法进行导入导出,充分利用 tree shaking 去除无用代码
  6. gzip 开启 gzip 压缩,进一步减少包体积
  7. 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。
`,593),E=[g];function k(q,v,f,D,w,x){return n(),a("div",null,E)}const P=s(h,[["render",k]]);export{_ as __pageData,P as default}; diff --git a/assets/front-end-engineering_performance.md.08504e29.lean.js b/assets/front-end-engineering_performance.md.e4e7bf85.lean.js similarity index 53% rename from assets/front-end-engineering_performance.md.08504e29.lean.js rename to assets/front-end-engineering_performance.md.e4e7bf85.lean.js index de4da70c..409f567d 100644 --- a/assets/front-end-engineering_performance.md.08504e29.lean.js +++ b/assets/front-end-engineering_performance.md.e4e7bf85.lean.js @@ -1 +1 @@ -import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets/2024-01-27-11-37-14.86d37b7f.png",o="/blog/assets/2024-01-27-11-42-53.5bf914cc.png",e="/blog/assets/2024-01-27-11-43-39.3f42a6fd.png",c="/blog/assets/2024-01-27-11-43-48.dc786c83.png",r="/blog/assets/2024-01-27-11-45-28.86477da9.png",t="/blog/assets/2024-01-28-16-38-33.db813e08.png",B="/blog/assets/2024-01-28-16-43-50.943cbfac.png",y="/blog/assets/2024-01-28-16-45-11.6c1597c5.png",i="/blog/assets/2024-01-28-16-52-03.61de8fe4.png",F="/blog/assets/2024-01-28-16-52-40.1ee5db60.png",u="/blog/assets/2024-01-28-17-09-16.389ef971.png",d="/blog/assets/2024-01-28-17-09-22.8a8ef8fe.png",b="/blog/assets/2024-01-28-17-18-12.4f54b024.png",A="/blog/assets/2024-01-28-17-28-28.709acd8d.png",m="/blog/assets/2024-01-28-17-29-13.fbcf0c26.png",C="/blog/assets/2024-01-28-17-30-23.3db19902.png",_=JSON.parse('{"title":"前端性能优化方法论","description":"","frontmatter":{},"headers":[{"level":2,"title":"Webpack 优化","slug":"webpack-优化","link":"#webpack-优化","children":[{"level":3,"title":"1. 构建性能","slug":"_1-构建性能","link":"#_1-构建性能","children":[]},{"level":3,"title":"2. 传输性能","slug":"_2-传输性能","link":"#_2-传输性能","children":[]},{"level":3,"title":"3. 运行性能","slug":"_3-运行性能","link":"#_3-运行性能","children":[]},{"level":3,"title":"4. webpack5 内置优化","slug":"_4-webpack5-内置优化","link":"#_4-webpack5-内置优化","children":[]},{"level":3,"title":"字节跳动面试题:说一下项目里有做过哪些 webpack 上的优化","slug":"字节跳动面试题-说一下项目里有做过哪些-webpack-上的优化","link":"#字节跳动面试题-说一下项目里有做过哪些-webpack-上的优化","children":[]}]},{"level":2,"title":"CSS","slug":"css","link":"#css","children":[]},{"level":2,"title":"网络层面","slug":"网络层面","link":"#网络层面","children":[{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]},{"level":3,"title":"CDN","slug":"cdn","link":"#cdn","children":[]},{"level":3,"title":"增加带宽","slug":"增加带宽","link":"#增加带宽","children":[]},{"level":3,"title":"http内置优化","slug":"http内置优化","link":"#http内置优化","children":[]},{"level":3,"title":"数据传输层面","slug":"数据传输层面","link":"#数据传输层面","children":[]}]},{"level":2,"title":"Vue","slug":"vue","link":"#vue","children":[{"level":3,"title":"Vue 开发优化","slug":"vue-开发优化","link":"#vue-开发优化","children":[]},{"level":3,"title":"Vue3 内置优化","slug":"vue3-内置优化","link":"#vue3-内置优化","children":[]},{"level":3,"title":"问题梳理","slug":"问题梳理","link":"#问题梳理","children":[]}]},{"level":2,"title":"React","slug":"react","link":"#react","children":[{"level":3,"title":"总结","slug":"总结-1","link":"#总结-1","children":[]}]},{"level":2,"title":"高性能JavaScript","slug":"高性能javascript","link":"#高性能javascript","children":[{"level":3,"title":"开发注意","slug":"开发注意","link":"#开发注意","children":[]},{"level":3,"title":"懒加载","slug":"懒加载","link":"#懒加载","children":[]},{"level":3,"title":"回流与重绘","slug":"回流与重绘","link":"#回流与重绘","children":[]},{"level":3,"title":"如何对项目中的图片进行优化?","slug":"如何对项目中的图片进行优化","link":"#如何对项目中的图片进行优化","children":[]}]},{"level":2,"title":"优化首屏响应","slug":"优化首屏响应","link":"#优化首屏响应","children":[{"level":3,"title":"觉得快","slug":"觉得快","link":"#觉得快","children":[]},{"level":3,"title":"真实快","slug":"真实快","link":"#真实快","children":[]}]}],"relativePath":"front-end-engineering/performance.md","lastUpdated":1710671080000}'),h={name:"front-end-engineering/performance.md"},g=l("",592),E=[g];function k(v,f,q,D,w,x){return n(),a("div",null,E)}const P=s(h,[["render",k]]);export{_ as __pageData,P as default}; +import{_ as s,o as n,c as a,a as l}from"./app.1ee4e414.js";const p="/blog/assets/2024-01-27-11-37-14.86d37b7f.png",o="/blog/assets/2024-01-27-11-42-53.5bf914cc.png",e="/blog/assets/2024-01-27-11-43-39.3f42a6fd.png",c="/blog/assets/2024-01-27-11-43-48.dc786c83.png",r="/blog/assets/2024-01-27-11-45-28.86477da9.png",t="/blog/assets/2024-01-28-16-38-33.db813e08.png",B="/blog/assets/2024-01-28-16-43-50.943cbfac.png",y="/blog/assets/2024-01-28-16-45-11.6c1597c5.png",i="/blog/assets/2024-01-28-16-52-03.61de8fe4.png",F="/blog/assets/2024-01-28-16-52-40.1ee5db60.png",u="/blog/assets/2024-01-28-17-09-16.389ef971.png",d="/blog/assets/2024-01-28-17-09-22.8a8ef8fe.png",b="/blog/assets/2024-01-28-17-18-12.4f54b024.png",A="/blog/assets/2024-01-28-17-28-28.709acd8d.png",m="/blog/assets/2024-01-28-17-29-13.fbcf0c26.png",C="/blog/assets/2024-01-28-17-30-23.3db19902.png",_=JSON.parse('{"title":"前端性能优化方法论","description":"","frontmatter":{},"headers":[{"level":2,"title":"Webpack 优化","slug":"webpack-优化","link":"#webpack-优化","children":[{"level":3,"title":"1. 构建性能","slug":"_1-构建性能","link":"#_1-构建性能","children":[]},{"level":3,"title":"2. 传输性能","slug":"_2-传输性能","link":"#_2-传输性能","children":[]},{"level":3,"title":"3. 运行性能","slug":"_3-运行性能","link":"#_3-运行性能","children":[]},{"level":3,"title":"4. webpack5 内置优化","slug":"_4-webpack5-内置优化","link":"#_4-webpack5-内置优化","children":[]},{"level":3,"title":"字节跳动面试题:说一下项目里有做过哪些 webpack 上的优化","slug":"字节跳动面试题-说一下项目里有做过哪些-webpack-上的优化","link":"#字节跳动面试题-说一下项目里有做过哪些-webpack-上的优化","children":[]}]},{"level":2,"title":"CSS","slug":"css","link":"#css","children":[]},{"level":2,"title":"网络层面","slug":"网络层面","link":"#网络层面","children":[{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]},{"level":3,"title":"CDN","slug":"cdn","link":"#cdn","children":[]},{"level":3,"title":"增加带宽","slug":"增加带宽","link":"#增加带宽","children":[]},{"level":3,"title":"http 内置优化","slug":"http-内置优化","link":"#http-内置优化","children":[]},{"level":3,"title":"数据传输层面","slug":"数据传输层面","link":"#数据传输层面","children":[]}]},{"level":2,"title":"Vue","slug":"vue","link":"#vue","children":[{"level":3,"title":"Vue 开发优化","slug":"vue-开发优化","link":"#vue-开发优化","children":[]},{"level":3,"title":"Vue3 内置优化","slug":"vue3-内置优化","link":"#vue3-内置优化","children":[]},{"level":3,"title":"问题梳理","slug":"问题梳理","link":"#问题梳理","children":[]}]},{"level":2,"title":"React","slug":"react","link":"#react","children":[{"level":3,"title":"总结","slug":"总结-1","link":"#总结-1","children":[]}]},{"level":2,"title":"高性能 JavaScript","slug":"高性能-javascript","link":"#高性能-javascript","children":[{"level":3,"title":"开发注意","slug":"开发注意","link":"#开发注意","children":[]},{"level":3,"title":"懒加载","slug":"懒加载","link":"#懒加载","children":[]},{"level":3,"title":"回流与重绘","slug":"回流与重绘","link":"#回流与重绘","children":[]},{"level":3,"title":"如何对项目中的图片进行优化?","slug":"如何对项目中的图片进行优化","link":"#如何对项目中的图片进行优化","children":[]}]},{"level":2,"title":"优化首屏响应","slug":"优化首屏响应","link":"#优化首屏响应","children":[{"level":3,"title":"觉得快","slug":"觉得快","link":"#觉得快","children":[]},{"level":3,"title":"真实快","slug":"真实快","link":"#真实快","children":[]}]}],"relativePath":"front-end-engineering/performance.md","lastUpdated":1718532121000}'),h={name:"front-end-engineering/performance.md"},g=l("",593),E=[g];function k(q,v,f,D,w,x){return n(),a("div",null,E)}const P=s(h,[["render",k]]);export{_ as __pageData,P as default}; diff --git a/fe-utils/git.html b/fe-utils/git.html index 8dbe9615..219eeeb1 100644 --- a/fe-utils/git.html +++ b/fe-utils/git.html @@ -198,7 +198,7 @@ 7. git push origin first 切回主分支:git checkout master

参考资料

local(每一个人的计算机上面也有一个项目仓库)

配置用户名

git config --global user.name 你的名字

配置邮箱

git config --global user.emial 你的邮箱

查看配置

- + diff --git "a/fe-utils/js\345\267\245\345\205\267\345\272\223.html" "b/fe-utils/js\345\267\245\345\205\267\345\272\223.html" index 55e87918..dea1d262 100644 --- "a/fe-utils/js\345\267\245\345\205\267\345\272\223.html" +++ "b/fe-utils/js\345\267\245\345\205\267\345\272\223.html" @@ -291,7 +291,7 @@ 例如: 2020-08-27T08:01:44.000Z

GMT、UTC、ISO 8601都表示的是零时区的时间

Unix 时间戳

Unix 时间戳(Unix Timestamp)是Unix系统最早提出的概念

它将UTC时间1970年1月1日凌晨作为起始时间,到指定时间经过的秒数(毫秒数)

程序中的时间处理

程序对时间的计算、存储务必使用UTC时间,或者时间戳

在和用户交互时,将UTC时间或时间戳转换为更加友好的文本

思考下面的问题:

  1. 用户的生日是本地时间还是UTC时间?
  2. 如果要比较两个日期的大小,是比较本地时间还是比较UTC时间?
  3. 如果要显示文章的发布日期,是显示本地时间还是显示UTC时间?
  4. 北京时间2020-8-28 10:00:00格林威治2020-8-28 02:00:00,两个时间哪个大,哪个小?
  5. 北京的时间戳为0格林威治的时间戳为0,它们的时间一样吗?
  6. 一个中国用户注册时填写的生日是1970-1-1,它出生的UTC时间是多少?时间戳是多少?

Moment的核心用法

Moment的使用分为两个部分:

  1. 获得Moment对象
  2. 针对Moment对象做各种操作
- + diff --git a/fe-utils/tool.html b/fe-utils/tool.html index a4b0e183..27a8e693 100644 --- a/fe-utils/tool.html +++ b/fe-utils/tool.html @@ -13,7 +13,7 @@
Skip to content
On this page

涌现出来的新tools

apifox:一款国产的 API 管理神器

集成了 Postman + Swagger + Mock + JMeter 众多功能

可以让生成 api 文档、api调试、api Mock、api 自动化测试 变的十分高效,简单。

eolink | 国内第一个集Swagger+Postman+Mock+Jmeter单点工具于一身的 api 管理平台 | 以 api 为中心的前后端开发流程

- + diff --git a/fragment/Monorepo.html b/fragment/Monorepo.html index b19a534a..7b38e2cc 100644 --- a/fragment/Monorepo.html +++ b/fragment/Monorepo.html @@ -13,7 +13,7 @@
Skip to content
On this page

monorepo

npm 的 install 流程

package-lock.json

package-lock.json 的作用是进行锁版本号,保证整个开发团队的版本号统一,使用 monorepo 的项目有可能会提到一个最外层进行一个管理。

  • 为什么需要这个 package-lock.json package.json 的 semantic versioning(语意化版本控制),在不同的时间会安装不同的版本,如果没有 package-lock.json,不同的开发者可能就会得到不同版本的依赖,如果因为这个出现了一个 bug 的话,那排查起来也许会非常困难。
  • 更新规则 Npm v 5.4.2 以上:当 package.json 声明的版本依赖规范和 package-lock.json 安装版本兼容,则根据 package-lock json 安装依赖:如果两者不兼容,那么按照 package.json 安装依赖,并更新 package- lock.json 跟随 package.json 的语意化版本控制来进行更新,如果 package.json 中的依赖 a 的版本是^1.0.0,在这个时候如果 package-lock.json 中的版本锁定为 1.12.1 就是符合要求的不必重写,但如果是 0.12.11 即第一个数字变了,那就需要重写 package-lock.json 了。
  • 版本规则
    • ^: 只会执行不更改最左边非零数字的更新。 如果写入的是 ^0.13.0,则当运行 npm update 时,可以更新到 0.13.1、0.13.2 等,但不能更新到 0.14.0 或更高版本。 如果写入的是 ^1.13.0,则当运行 npm update 时,可以更新到 1.13.1、1.14.0 等,但不能更新到 2.0.0 或更高版本。
    • ~: 如果写入的是 〜0.13.0,则当运行 npm update 时,会更新到补丁版本:即 0.13.1 可以,但 0.14.0 不可以。
    • : 接受高于指定版本的任何版本。

    • =: 接受等于或高于指定版本的任何版本。

    • =: 接受确切的版本。
    • -: 接受一定范围的版本。例如:2.1.0 - 2.6.2。
    • ||: 组合集合。例如 < 2.1 || > 2.6。

npm 和 yarn

缺点。下面将两者进行比较

  1. 性能

每当 Yarn 或 npm 需要安装包时,它们都会执行一系列任务。在 npm 中,这些任务是按包顺序执行的,这意味着它会等待一个包完全安装,然后再继续下一个。相比之下,Yarn 并行执行这些任务,从而提高了性能。 虽然这两个管理器都提供缓存机制,但 Yarn 似乎做得更好一些。 尽管 Yarn 有一些优势,但 Yarn 和 npm 在它们的最新版本中的速度相当。所以我们不能评判孰优孰劣。

  1. 依赖版本

早期的时候 yarn 有 yarn.lock 来锁定版本,这一点上比 package.json 要强很多,而后面 npm 也推出了 package-lock.json,所以这一点上已经没太多差异了。

  1. 安全性

从版本 6 开始,npm 会在安装过程中审核软件包并告诉您是否发现了任何漏洞。我们可以通过 npm audit 针对已安装的软件包运行来手动执行此检查。如果发现任何漏洞,npm 会给我们安全建议。 Yarn 和 npm 都使用加密哈希算法来确保包的完整性。

  1. 工作区

工作区允许您拥有一个 monorepo 来管理跨多个项目的依赖项。这意味着您有一个单一的顶级根包,其中包含多个称为工作区的子包。

  1. 用哪个?

目前 2021 年,yarn 的安装速度还是比 npm 快,其他地方的差异并不大,基本上可以忽略,用哪个都行。

monorepo

monorepo 方案的优势

  1. 代码重用将变得非常容易:由于所有的项目代码都集中于一个代码仓库,我们将很容易抽离出各个项目共用的业务组件或工具,并通过 TypeScript,Lerna 或其他工具进行代码内引用;
  2. 依赖管理将变得非常简单:同理,由于项目之间的引用路径内化在同一个仓库之中,我们很容易追踪当某个项目的代码修改后,会影响到其他哪些项目。通过使用一些工具,我们将很容易地做到版本依赖管理和版本号自动升级;
  3. 代码重构将变得非常便捷:想想究竟是什么在阻止您进行代码重构,很多时候,原因来自于「不确定性」,您不确定对某个项目的修改是否对于其他项目而言是「致命的」,出于对未知的恐惧,您会倾向于不重构代码,这将导致整个项目代码的腐烂度会以惊人的速度增长。而在 monorepo 策略的指导下,您能够明确知道您的代码的影响范围,并且能够对被影响的项目可以进行统一的测试,这会鼓励您不断优化代码;
  4. 它倡导了一种开放,透明,共享的组织文化,这有利于开发者成长,代码质量的提升:在 monorepo 策略下,每个开发者都被鼓励去查看,修改他人的代码(只要有必要),同时,也会激起开发者维护代码,和编写单元测试的责任心(毕竟朋友来访之前,我们从不介意自己的房子究竟有多乱),这将会形成一种良性的技术氛围,从而保障整个组织的代码质量。

monorepo 方案的劣势

  1. 项目粒度的权限管理变得非常复杂:无论是 Git 还是其他 VCS 系统,在支持 monorepo 策略中项目粒度的权限管理上都没有令人满意的方案,这意味着 A 部门的 a 项目若是不想被 B 部门的开发者看到就很难了。(好在我们可以将 monorepo 策略实践在「项目级」这个层次上,这才是我们这篇文章的主题,我们后面会再次明确它);
  2. 新员工的学习成本变高:不同于一个项目一个代码仓库这种模式下,组织新人只要熟悉特定代码仓库下的代码逻辑,在 monorepo 策略下,新人可能不得不花更多精力来理清各个代码仓库之间的相互逻辑,当然这个成本可以通过新人文档的方式来解决,但维护文档的新鲜又需要消耗额外的人力;
  3. 对于公司级别的 monorepo 策略而言,需要专门的 VFS 系统,自动重构工具的支持:设想一下 Google 这样的企业是如何将十亿行的代码存储在一个仓库之中的?开发人员每次拉取代码需要等待多久?各个项目代码之间又如何实现权限管理,敏捷发布?任何简单的策略乘以足够的规模量级都会产生一个奇迹(不管是好是坏),对于中小企业而言,如果没有像 Google,Facebook 这样雄厚的人力资源,把所有项目代码放在同一个仓库里这个美好的愿望就只能是个空中楼阁。

如何取舍?

没错,软件开发领域从来没有「银弹」。monorepo 策略也并不完美,并且,我在实践中发现,要想完美在组织中运用 monorepo 策略,所需要的不仅是出色的编程技巧和耐心。团队日程,组织文化和个人影响力相互碰撞的最终结果才决定了想法最终是否能被实现。

但是请别灰心的太早,因为虽然让组织作出改变,统一施行 monorepo 策略困难重重,但这却并不意味着我们需要彻底跟 monorepo 策略说再见。我们还可以把 monorepo 策略实践在「项目」这个级别,即从逻辑上确定项目与项目之间的关联性,然后把相关联的项目整合在同一个仓库下,通常情况下,我们不会有太多相互关联的项目,这意味着我们能够免费得到 monorepo 策略的所有好处,并且可以拒绝支付大型 monorepo 架构的利息。

- + diff --git a/fragment/api-no-repeat.html b/fragment/api-no-repeat.html index 3b3a4155..298f65ca 100644 --- a/fragment/api-no-repeat.html +++ b/fragment/api-no-repeat.html @@ -309,7 +309,7 @@ return Object.prototype.toString.call(config.data) === "[object FormData]"; }

demo

- + diff --git a/fragment/auto-try-catch.html b/fragment/auto-try-catch.html index d6984e24..5f335373 100644 --- a/fragment/auto-try-catch.html +++ b/fragment/auto-try-catch.html @@ -891,7 +891,7 @@ toArray, };

本文章作者的 github 仓库

- + diff --git a/fragment/babel-console.html b/fragment/babel-console.html index bf0ee1b8..c5b0a5e3 100644 --- a/fragment/babel-console.html +++ b/fragment/babel-console.html @@ -543,7 +543,7 @@ }; }; - + diff --git a/fragment/const.html b/fragment/const.html index 77233ae9..261d8ab6 100644 --- a/fragment/const.html +++ b/fragment/const.html @@ -75,7 +75,7 @@ } a = 20; // 报错 - + diff --git a/fragment/disable-debugger.html b/fragment/disable-debugger.html index 11566697..a1ba7d7d 100644 --- a/fragment/disable-debugger.html +++ b/fragment/disable-debugger.html @@ -179,7 +179,7 @@ } catch (err) {} })(); - + diff --git a/fragment/fetch-pause.html b/fragment/fetch-pause.html index 2698c293..ba0d6596 100644 --- a/fragment/fetch-pause.html +++ b/fragment/fetch-pause.html @@ -147,7 +147,7 @@ result.resume(); }, 4000);

执行原理

流程设计上是这样,设计一个控制器,发起请求并请求返回后,判断控制器的状态,若控制器不处于 “暂停” 状态时,正常返回数据;当控制器处于 “暂停状态” 时,控制器将置为 “一旦调用恢复方法就返回数据” 的状态。 代码中是利用了 Promise.all 捆绑一个控制器 Promise ,如果控制器处于暂停状态下,则不释放 Promise.all ,再将对应的 pause 方法和 resume 方法暴露出去给外界使用。

补充

有些同学错误的认为网络请求和响应是绝对不可以暂停的,我特意在文章前面提到了有关数据传输的内容,并且挂了一句“理论上应用层的协议可以通过类似于标记数据包序列号等等一系列手段来实现暂停机制”,这句话的意思是,如果你魔改 HTTP 或者自己设计实现一个应用层协议(例如像 socket、vmess 这些协议),只要双端支持该协议,是可以实现请求暂停或者响应暂停的,而且这不会影响到 TCP 连接,但是实现暂停机制需要对各种场景和 TCP 策略兜底才能有较好的可靠性。 例如,提供一类控制报文用于控制传输暂停,首先需要对所有数据包的序列号标记顺序,当需要暂停时,发送该序列号的暂停报文给接收端,接收端收到暂停报文就将已接收数据包的块标记返回给发送端等等(这和分片上传机制一样)。

- + diff --git a/fragment/fetch.html b/fragment/fetch.html index 1ce7b658..33876e8f 100644 --- a/fragment/fetch.html +++ b/fragment/fetch.html @@ -109,7 +109,7 @@ }; }

说实话,我对取消 Fetch 的方法并不感到兴奋。在理想的世界中,通过 Fetch 返回的 Promise 中的 .cancel() 会很酷,但是也会带来一些问题。无论如何,我为能够取消 Fetch 调用而感到高兴,你也应该如此!

[译] 如何取消你的 Promise?

- + diff --git a/fragment/forEach.html b/fragment/forEach.html index ddbea89e..475dba12 100644 --- a/fragment/forEach.html +++ b/fragment/forEach.html @@ -95,7 +95,7 @@ Array.prototype.every(); Array.prototype.some();

如何根据不同的业务场景,选择使用对应的工具函数来更有效地处理业务逻辑,才是我们真正应该思考的,或许这也是面试当中真正想考察的吧。

- + diff --git a/fragment/nextTick.html b/fragment/nextTick.html index ccc432ed..98fbabef 100644 --- a/fragment/nextTick.html +++ b/fragment/nextTick.html @@ -259,7 +259,7 @@ // 否则就可能出现一直循环的情况, // 所以需要将 callbacks 复制一份出来然后清空,再遍历备份列表执行回调 - + diff --git a/fragment/npm-scripts.html b/fragment/npm-scripts.html index fbafe2df..107f467a 100644 --- a/fragment/npm-scripts.html +++ b/fragment/npm-scripts.html @@ -31,7 +31,7 @@ "dev": "./node_modules/.bin/nodemon bin/www" }

当执行 npm run dev 时,相当于执行 "./node_modules/.bin/nodemon bin/www",其中 bin/www 作为参数传入。

总之:先去 package.json 的 scripts 属性中查找 xxx1,再去./node_modules/.bin 中查找 xxx1 对应的 sss1,执行 sss1 文件)

问题分析

TIP

问题 1: 为什么不直接执行 sss 而要执行 xxx 呢?

直接执行 sss 会报错,因为操作系统中不存在 sss 这一条指令。

TIP

问题 2: 为什么执行 npm run xxx(或者 yarn xxx)时就能成功呢?

因为我们在安装依赖的时候,是通过 npm i xxx (或者 yarn ...)来执行的,例如 npm i @vue/cli-service,npm 在 安装这个依赖的时候,就会 node_modules/.bin/ 目录中创建好名为 vue-cli-service 的几个可执行文件了。

.bin 目录下的文件,是一个个的软链接,打开文件可以看到文件顶部写着 #!/bin/sh ,表示这是一个脚本,可由 node 来执行。当使用 npm run xxx 执行 sss 时,虽然没有安装 sss 的全局命令,但是 npm 会到 ./node_modules/.bin 中找到 sss 文件作为脚本来执行,则相当于执行了 ./node_modules/.bin/sss。

即: 运行 npm run xxx 的时候,npm 会先在当前目录的 node_modules/.bin 查找要执行的程序,如果找到则运行;没有找到则从全局的 node_modules/.bin 中查找; 全局目录还是没找到,就会从 window 的 path 环境变量中查找有没有其他同名的可执行程序。

浅析 npm install 机制阅读

- + diff --git a/fragment/promise-cancel.html b/fragment/promise-cancel.html index 4d3837ee..828b9fbc 100644 --- a/fragment/promise-cancel.html +++ b/fragment/promise-cancel.html @@ -365,7 +365,7 @@ p.notify((x) => console.log(x)); p.then((res) => console.log("Get All Data:", res));

End

关于取消功能在红宝书上 TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。结果 ES6 Promise 被认为是“激进的”:只要 Promise 的逻辑开始执行,就没有办法阻止它执行到完成。

实际上我们学了这么久的 Promise 也默认了这一点,因此这个取消功能反而就不太符合常理,而且十分鸡肋。比如说我们有使用 then 回调接收数据,但因为你点击了取消按钮造成 then 回调不执行,我们知道 Promise 支持链式调用,那如果还有后续操作都将会被中断,这种中断行为 debug 时也十分痛苦,更何况最麻烦的一点是你还需要传入一个 delay 来表示取消的期限,而这个期限到底要设置多少才合适呢...

- + diff --git a/fragment/react-duplicate.html b/fragment/react-duplicate.html index 37d5c6e4..9403548e 100644 --- a/fragment/react-duplicate.html +++ b/fragment/react-duplicate.html @@ -37,7 +37,7 @@ }, }; - + diff --git a/fragment/react-hooks-timer.html b/fragment/react-hooks-timer.html index 09b07644..ed986bca 100644 --- a/fragment/react-hooks-timer.html +++ b/fragment/react-hooks-timer.html @@ -223,7 +223,7 @@ export default CountDown;

错误示例会出现的问题

原因

解决方案

如上示例 1 , 2 ,因为方式 1 打破了 React 纯函数的规则,所以更加建议方式 2

TIP

第一段代码使用了普通的变量 cd 和 timer,它们被定义在组件函数的顶层。这意味着每次组件函数被调用时,都会重新创建新的 cd 和 timer,而不是保留它们的状态。这样做违反了 React 纯函数组件的规则,因为它引入了外部的状态管理。

第二段代码使用了 useRef 来创建了 cd 和 timer 这两个变量的引用。这意味着它们会被保存在组件的生命周期之外,并且在组件的多次渲染之间保持不变。因此,即使组件函数被重新调用,这些引用的值也会保持不变。这样做遵守了 React 纯函数组件的规则,因为它不引入外部状态管理。

因此,第二段代码符合 React 纯函数组件的规则,而第一段代码打破了这些规则。

【注意】

- + diff --git a/fragment/react-useState.html b/fragment/react-useState.html index ab776f69..3f96e9b2 100644 --- a/fragment/react-useState.html +++ b/fragment/react-useState.html @@ -367,7 +367,7 @@

首先,在 useSyncCallback 中创建一个标示 proxyState,初始的时候会把 proxyState 的 current 值赋成 false,在 callback 执行之前会先判断 current 是否为 true,如果为 true 就允许 callback 执行,若果为 false,就跳过不执行,因为 useEffect 在组件 render 之后,只要依赖项有变化就会执行,所以我们无法掌控我们的函数执行,在 useSyncCallback 中创建一个新的函数 Func,并返回,通过这个 Func 来模拟函数调用,

js
const [proxyState, setProxyState] = useState({ current: false });
 
const [proxyState, setProxyState] = useState({ current: false });
 

在这个函数中我们要做的就是变更 prxoyState 的 current 值为 true,来使得让 callback 被调用的条件成立,同时触发 react 组件 render 这样内部的 useEffect 就会执行,随后调用 callback 实现我们想要的效果。

- + diff --git a/fragment/return-await.html b/fragment/return-await.html index 8f0a07c6..b8083844 100644 --- a/fragment/return-await.html +++ b/fragment/return-await.html @@ -97,7 +97,7 @@ foo2(); // undefined

如果想要在调试的堆栈中得到 bar() 抛出的错误信息,那么此时应该使用 return await 。

- + diff --git a/fragment/setTimeout.html b/fragment/setTimeout.html index 27638f20..6610f8a2 100644 --- a/fragment/setTimeout.html +++ b/fragment/setTimeout.html @@ -105,7 +105,7 @@ }, speed); } - + diff --git a/fragment/tree-shaking.html b/fragment/tree-shaking.html index c7dcbc71..f06990ed 100644 --- a/fragment/tree-shaking.html +++ b/fragment/tree-shaking.html @@ -13,7 +13,7 @@
Skip to content
- + diff --git a/fragment/useRequest.html b/fragment/useRequest.html index 43ce8675..2d74888a 100644 --- a/fragment/useRequest.html +++ b/fragment/useRequest.html @@ -265,7 +265,7 @@ }, [config, ready]); }; - + diff --git a/fragment/var-array.html b/fragment/var-array.html index 5be5c18f..1683181d 100644 --- a/fragment/var-array.html +++ b/fragment/var-array.html @@ -69,7 +69,7 @@ return Object.values(this)[Symbol.iterator](); };

这段代码是将 Object.prototype 上的 [Symbol.iterator] 方法重新定义为一个新的函数。新的函数通过调用 Object.values(this) 方法获取对象的所有值,并返回这些值的迭代器对象。 通过这个代码,我们可以使得任何 JavaScript 对象都具有了迭代能力。例如,对于一个对象 obj ,我们可以直接使用 for...of 循环或者 ... 操作符来遍历它的所有值。

- + diff --git a/fragment/video.html b/fragment/video.html index 40f6b794..574f90d0 100644 --- a/fragment/video.html +++ b/fragment/video.html @@ -527,7 +527,7 @@ }); }

以上就是回放的关键流程实现代码, rrweb 中不仅仅是做了这些,还包含数据压缩,移动端处理,隐私问题等等细节处理,有兴趣可自行查看源码。

- + diff --git "a/fragment/\345\276\256\345\206\205\346\240\270\346\236\266\346\236\204.html" "b/fragment/\345\276\256\345\206\205\346\240\270\346\236\266\346\236\204.html" index 5e47f564..2ed43089 100644 --- "a/fragment/\345\276\256\345\206\205\346\240\270\346\236\266\346\236\204.html" +++ "b/fragment/\345\276\256\345\206\205\346\240\270\346\236\266\346\236\204.html" @@ -285,7 +285,7 @@ install(app, options) {}, };

类似 jQuery、window 从某种角度上来说也可以看做是内核,通过 $.fn() 或者在 prototype 中扩展方法也是一种插件的形式。

小结

最后我们再来巩固一下这篇文章的核心思想,微内核架构主要由两部分组成: Core + Plugin,其中: Core 需要具备的能力有:

🔗 原文链接: https://juejin.cn/post/716307803160...

- + diff --git "a/fragment/\346\216\245\345\217\243\350\256\276\350\256\241.html" "b/fragment/\346\216\245\345\217\243\350\256\276\350\256\241.html" index 0043ef60..e105c556 100644 --- "a/fragment/\346\216\245\345\217\243\350\256\276\350\256\241.html" +++ "b/fragment/\346\216\245\345\217\243\350\256\276\350\256\241.html" @@ -13,7 +13,7 @@
Skip to content
On this page

接口设计

防止用户伪造请求:如果接口允许用户直接携带 ID,很容易被恶意用户利用,尝试修改其他用户的信息。使用 cookie(尤其是带有认证信息的 cookie)可以确保请求来自于正确的用户,并且认证信息通常在服务器端进行验证,增加了安全性。

- + diff --git "a/fragment/\346\262\231\347\233\222.html" "b/fragment/\346\262\231\347\233\222.html" index ebe9d3cd..d9297937 100644 --- "a/fragment/\346\262\231\347\233\222.html" +++ "b/fragment/\346\262\231\347\233\222.html" @@ -463,7 +463,7 @@ let img = new Image(); img.src = "http://www.test.com/img.gif";

黑名单中添加'Image'字段,堵上这个漏洞

- + diff --git "a/fragment/\351\273\221\347\231\275.html" "b/fragment/\351\273\221\347\231\275.html" index a58454fb..1a46e81a 100644 --- "a/fragment/\351\273\221\347\231\275.html" +++ "b/fragment/\351\273\221\347\231\275.html" @@ -49,7 +49,7 @@ -webkit-filter: grayscale(0.95); }

filter 样式加到 html 还是 body 上

把 filter 样式加到了 <body> 元素上。通常这没有问题。

但如果你的网页内有「绝对和固定定位」元素,一定要把 filter 样式加到 <html> 上。 原因见: drafts.fxtf.org/filter-effe…

引用:

A value other than none for the filter property results in the creation of a >containing block for absolute and fixed positioned descendants unless the element it > applies to is a document root element in the current browsing context.

翻译:

若 filter 属性的值不是 none,会给「绝对和固定定位的后代」创建一个 containing block

除非 filter 对应的元素是「当前浏览上下文中的文档根元素」(即 <html> )。

因此,兼容性最好的方法是把 filter 样式加到 <html> 上。这样不会影响「绝对和固定定位的后代」。 这里小程序有个坑,如果你的页面代码有「绝对和固定定位的后代」,就不能把 filter 样式 加到 <page> 上,而是要找个元素,这个元素没有「绝对和固定定位的后代」,你可以把 filter 样式加到这个元素上。

- + diff --git "a/front-end-engineering/CSS \351\242\204\345\244\204\347\220\206\345\231\250\344\271\213SCSS.html" "b/front-end-engineering/CSS \351\242\204\345\244\204\347\220\206\345\231\250\344\271\213SCSS.html" index c323da7c..2335ccb8 100644 --- "a/front-end-engineering/CSS \351\242\204\345\244\204\347\220\206\345\231\250\344\271\213SCSS.html" +++ "b/front-end-engineering/CSS \351\242\204\345\244\204\347\220\206\345\231\250\344\271\213SCSS.html" @@ -2981,7 +2981,7 @@ .....; }
  1. 多使用混合指令:混合指令可以将公共的部分抽离出来,提高了代码的复用性。但是要清楚混合指令和 @extend 之间的区别,具体使用哪一个,取决于你写项目时的具体场景,不是说某一个就比另一个绝对的好。
  2. 使用函数:可以编写自定义函数来处理一些复杂的计算和操作。而且 Sass 还提供了很多非常好用的内置函数。
  3. 遵循常见的 Sass 编码规范:

Sass 未来发展

我们如果想要获取到 Sass 的最新动向,通常可以去 Sass 的社区看一下。

注意:一门成熟的技术,是一定会有对应社区的。理论上来讲,社区的形式是不限的,但是通常是以论坛的形式存在的,大家可以在论坛社区自由的讨论这门技术相关的话题。

社区往往包含了这门技术最新的动态,甚至有一些优秀的技术解决方案是先来自于社区,之后才慢慢成为正式的标准语法的。

目前市面上又很多 CSS 库都是基于 Sass 来进行构建了,例如:

  1. Compass - 老牌 Sass 框架,提供大量 Sass mixins 和函数,方便开发。
  2. Bourbon - 轻量级的 Sass mixin 库,提供常用的 mixins,简化 CSS 开发。
  3. Neat - 构建具有响应式网格布局的网站,基于 SassBourbon,容易上手。
  4. Materialize - 实现 Material Design 风格,基于 Sass 构建,提供丰富组件和元素。
  5. Bulma - 现代 CSS 框架,提供弹性网格和常见组件,可与 Sass 一起使用。
  6. Foundation - 老牌前端框架,基于 Sass,提供全面的组件和工具,适合构建复杂项目。
  7. Semantic UI - 设计美观的 UI 套件,基于 Sass 构建,提供丰富样式和交互。
  8. Spectre.css - 轻量级、响应式和现代的 CSS 框架,可以与 Sass 结合使用。

因此,基本上目前 Sass 已经成为了前端开发人员首选的 CSS 预处理器。因为 Sass 相比其他两个 CSS 预处理器,功能是最强大的,特性是最多的,社区也是最活跃的。

关于 Sass 官方团队,未来再对 Sass 进行更新的时候,基本上会往以下几个方面做出努力:

本文所有源码均在 https://github.com/Sunny-117/blog/tree/main/code/sass-demo

- + diff --git "a/front-end-engineering/CSS\345\267\245\347\250\213\345\214\226.html" "b/front-end-engineering/CSS\345\267\245\347\250\213\345\214\226.html" index 7b4dd501..68fbb8c9 100644 --- "a/front-end-engineering/CSS\345\267\245\347\250\213\345\214\226.html" +++ "b/front-end-engineering/CSS\345\267\245\347\250\213\345\214\226.html" @@ -677,7 +677,7 @@ ], };

配置生成的文件名

output.filename的含义一样,即根据 chunk 生成的样式文件名

配置生成的文件名,例如[name].[contenthash:5].css

默认情况下,每个 chunk 对应一个 css 文件

原子化 CSS(了解)

- + diff --git a/front-end-engineering/PackageManager.html b/front-end-engineering/PackageManager.html index afcefb2b..c4127269 100644 --- a/front-end-engineering/PackageManager.html +++ b/front-end-engineering/PackageManager.html @@ -247,7 +247,7 @@

nvm

nvm 并非包管理器,它是用于管理多个 node 版本的工具

在实际的开发中,可能会出现多个项目分别使用的是不同的 node 版本,在这种场景下,管理不同的 node 版本就显得尤为重要

nvm 就是用于切换版本的一个工具

1.下载和安装

最新版下载地址:https://github.com/coreybutler/nvm-windows/releases

下载 nvm-setup.zip 后,直接安装 一路下一步,不要改动安装路径(开发类工具尽量不该动)

2.使用 nvm

nvm 提供了 CLI 工具,用于管理 node 版本

管理员运行输入 nvm,以查看各种可用命令 nvm arch:打印系统版本和默认 node 架构类型 nvm install 8.5.4:nvm 安装指定的 node 版本 nvm list :列出目前电脑上使用的以及已经安装过的那些 node 版本 npm list available:node 版本

为了加快下载速度,建议设置淘宝镜像 node 淘宝镜像:https://npm.taobao.org/mirrors/node/ npm 淘宝镜像:https://npm.taobao.org/mirrors/npm/

nvm node _mirror https://npm.taobao.org/mirrors/node/ nvm npm_mirror https://npm.taobao.org/mirrors/npm/ 查看全局安装包:npm -g list --depth=0 切换 node 版本:nvm use 10.18.0 看 node 版本:node -v 卸载:nvm uninstall 10.18.0

pnpm

pnpm 是一种新起的包管理器,从 npm 的下载量看,目前还没有超过 yarn,但它的实现方式值得主流包管理器学习,某些开发者极力推荐使用 pnpm

从结果上来看,它具有以下优势:

  1. 目前,安装效率高于 npm 和 yarn 的最新版
  2. 极其简洁的 node_modules 目录
  3. 避免了开发时使用间接依赖的问题(之前的包管理器可以使用间接依赖不报错)pnpm 没有把间接依赖直接放入 node_modules 里面
  4. 能极大的降低磁盘空间的占用
  5. 使用缓存

1.安装和使用

全局安装 pnpm

shell
npm install -g pnpm
 
npm install -g pnpm
 

之后在使用时,只需要把 npm 替换为 pnpm 即可

如果要执行安装在本地的 CLI,可以使用 pnpx,它和 npx 的功能完全一样,唯一不同的是,在使用 pnpx 执行一个需要安装的命令时,会使用 pnpm 进行安装

比如npx mocha执行本地的mocha命令时,如果mocha没有安装,则 npx 会自动的、临时的安装 mocha,安装好后,自动运行 mocha 命令 类似:npx create-react-app my-app 临时下载 create-react-app,然后自动运行命令

2.pnpm 原理

  1. 同 yarn 和 npm 一样,pnpm 仍然使用缓存来保存已经安装过的包,以及使用 pnpm-lock.yaml 来记录详细的依赖版本

缓存位于工程所在盘的根目录,所以位置不固定

  1. 不同于 yarn 和 npm, pnpm 使用符号链接和硬链接(可将它们想象成快捷方式)的做法来放置依赖,从而规避了从缓存中拷贝文件的时间,使得安装和卸载的速度更快
  2. 由于使用了符号链接和硬链接,pnpm 可以规避 windows 操作系统路径过长的问题,因此,它选择使用树形的依赖结果,有着几乎完美的依赖管理。也因为如此,项目中只能使用直接依赖,而不能使用间接依赖

3.注意事项

由于 pnpm 会改动 node_modules 目录结构,使得每个包只能使用直接依赖,而不能使用间接依赖,因此,如果使用 pnpm 安装的包中包含间接依赖,则会出现问题(现在不会了,除非使用了绝对路径)

由于 pnpm 超高的安装卸载效率,越来越多的包开始修正之前的间接依赖代码

bower

浏览器端

过时了

- + diff --git a/front-end-engineering/engineering-onepage.html b/front-end-engineering/engineering-onepage.html index 4ebb6b61..e1e4ee37 100644 --- a/front-end-engineering/engineering-onepage.html +++ b/front-end-engineering/engineering-onepage.html @@ -475,7 +475,7 @@ # 设置缓存位置 npm config set cache "新的缓存路径" - + diff --git a/front-end-engineering/jscompatibility.html b/front-end-engineering/jscompatibility.html index d96e731c..0e4be389 100644 --- a/front-end-engineering/jscompatibility.html +++ b/front-end-engineering/jscompatibility.html @@ -225,7 +225,7 @@ extends: 'airbnb' # 配置继承自 airbnb }

企业开发的实际情况

我们要做什么?

- + diff --git a/front-end-engineering/modularization.html b/front-end-engineering/modularization.html index 7a3fb0f3..0eea54b6 100644 --- a/front-end-engineering/modularization.html +++ b/front-end-engineering/modularization.html @@ -245,7 +245,7 @@ export { k, default, a as m2a } from "./m2.js"; export const r = "m-r"; - + diff --git a/front-end-engineering/node.html b/front-end-engineering/node.html index 5d0d7d98..d7733d33 100644 --- a/front-end-engineering/node.html +++ b/front-end-engineering/node.html @@ -13,7 +13,7 @@
Skip to content
On this page

Node 组成原理

Node.js 是一个开源的、跨平台的 JavaScript 运行环境,依赖于 Google V8 引擎,用于构建高性能的网络应用程序。Node.js 采用事件驱动、非阻塞 I/O 模型,使得它能够处理大量并发连接,适用于构建实时应用、高吞吐量的后端服务和网络代理等。

Node.js 广泛应用于 Web 开发、服务器端开发、实时通信、大数据处理等领域,被许多大型互联网公司和开发者使用和推崇。

Node.js 的特点包括:

  1. 单线程和事件驱动:Node.js 采用单线程的事件循环模型,通过异步 I/O 和事件驱动处理并发请求,避免了传统多线程模型中的线程切换和资源开销,提高了性能和可扩展性。
  2. 跨平台:Node.js 可运行于多个操作系统平台,包括 Windows、Linux 和 Mac OS 等。
  3. 高性能:由于基于 V8 引擎和非阻塞 I/O 模型,Node.js 具有快速的执行速度和高吞吐量,适用于处理大量并发请求的场景。
  4. 模块化和包管理:Node.js 支持模块化开发,可以通过 npm(Node Package Manager)进行包的管理和发布,方便了代码的组织和复用。
  5. 强大的社区支持:Node.js 拥有庞大的开发者社区,提供了丰富的第三方模块和工具,方便开发者进行开发和调试。

Node.js 组成

  1. 用户代码:JS 代码,开发者编写的
  2. 第三方库:大部分仍然是 JS 代码,由其他开发者编写
  3. 本地模块:Node.js 内置了一些核心模块,这些模块提供了基础的功能,如文件操作(fs 模块)、网络通信(http 模块)、加密(crypto 模块)、操作系统信息(os 模块)等。这些模块可以直接通过 require 函数进行引入使用。
  4. 内置模块:Node.js 有一个丰富的第三方模块生态系统,开发者可以通过 NPM 安装这些模块,并在自己的项目中引入使用。
  5. libuv:libuv 是一个跨平台的异步 I/O 库,它为 Node.js 提供了非阻塞的事件驱动的 I/O 操作。它可以处理文件系统操作、网络请求、定时器等等,在 Node.js 中用于处理事件循环。
  6. os api:将 Node.js 可运行于多个操作系统平台,包括 Windows、Linux 和 Mac OS 等。
  7. V8 引擎:Node.js 使用了 Google 开发的 V8 引擎作为其 JavaScript 执行引擎。V8 引擎可以将 JavaScript 代码直接转化为机器码,以提供高性能的执行效率。(c/c++代码,作用:把 JS 代码解释成为机器码。可以通过 v8 引擎的某种机制,扩展其功能。V8 引擎的扩展和对扩展的编译,是通过一个工具:gyp 工具。某些第三方库需要使用 node-gyp 工具进行构建,因此需要先安装 node-gyp)

其他资料

https://github.com/theanarkh/understand-nodejs

- + diff --git a/front-end-engineering/performance.html b/front-end-engineering/performance.html index 061b6eaa..c5ca73ba 100644 --- a/front-end-engineering/performance.html +++ b/front-end-engineering/performance.html @@ -7,77 +7,77 @@ - + -
Skip to content
On this page

前端性能优化方法论

我们可以从两个方面来看性能优化的意义:

  1. 用户角度:网站优化能够让页面加载得更快,响应更加及时,极大提升用户体验。
  2. 服务商角度:优化会减少页面资源请求数,减小请求资源所占带宽大小,从而节省可观的带宽资源。

网站优化的目标就是减少网站加载时间,提高响应速度。 Google 和亚马逊的研究表明,Google 页面加载的时间从 0.4 秒提升到 0.9 秒导致丢失了 20% 流量和广告收入,对于亚马逊,页面加载时间每增加 100ms 就意味着 1% 的销售额损失。 可见,页面的加载速度对于用户有着至关重要的影响。

Webpack 优化

如何分析打包结果?webpack-bundle-analyzer

1. 构建性能

TIP

这里所说的构建性能,是指在开发阶段的构建性能,而不是生产环境的构建性能

优化的目标,是降低从打包开始,到代码效果呈现所经过的时间

构建性能会影响开发效率。构建性能越高,开发过程中时间的浪费越少

1.1 减少模块解析

模块解析包括:抽象语法树分析、依赖分析、模块语法替换

如果某个模块不做解析,该模块经过loader处理后的代码就是最终代码。

如果没有loader对该模块进行处理,该模块的源码就是最终打包结果的代码。

如果不对某个模块进行解析,可以缩短构建时间,那么哪些模块不需要解析呢?

模块中无其他依赖:一些已经打包好的第三方库,比如jquery,所以可以配置module.noParse,它是一个正则,被正则匹配到的模块不会解析

js
module.exports = {
-  mode: 'development',
+    
Skip to content
On this page

前端性能优化方法论

https://juejin.cn/post/6993137683841155080

我们可以从两个方面来看性能优化的意义:

  1. 用户角度:网站优化能够让页面加载得更快,响应更加及时,极大提升用户体验。
  2. 服务商角度:优化会减少页面资源请求数,减小请求资源所占带宽大小,从而节省可观的带宽资源。

网站优化的目标就是减少网站加载时间,提高响应速度。 Google 和亚马逊的研究表明,Google 页面加载的时间从 0.4 秒提升到 0.9 秒导致丢失了 20% 流量和广告收入,对于亚马逊,页面加载时间每增加 100ms 就意味着 1% 的销售额损失。 可见,页面的加载速度对于用户有着至关重要的影响。

Webpack 优化

如何分析打包结果?webpack-bundle-analyzer

1. 构建性能

TIP

这里所说的构建性能,是指在开发阶段的构建性能,而不是生产环境的构建性能

优化的目标,是降低从打包开始,到代码效果呈现所经过的时间

构建性能会影响开发效率。构建性能越高,开发过程中时间的浪费越少

1.1 减少模块解析

模块解析包括:抽象语法树分析、依赖分析、模块语法替换

如果某个模块不做解析,该模块经过 loader 处理后的代码就是最终代码。

如果没有 loader 对该模块进行处理,该模块的源码就是最终打包结果的代码。

如果不对某个模块进行解析,可以缩短构建时间,那么哪些模块不需要解析呢?

模块中无其他依赖:一些已经打包好的第三方库,比如 jquery,所以可以配置 module.noParse,它是一个正则,被正则匹配到的模块不会解析

js
module.exports = {
+  mode: "development",
   module: {
-    noParse: /jquery/
-  }
-}
+    noParse: /jquery/,
+  },
+};
 
module.exports = {
-  mode: 'development',
+  mode: "development",
   module: {
-    noParse: /jquery/
-  }
-}
-

1.2 优化loader性能

  1. 进一步限制loader的应用范围。对于某些库,不使用loader

例如:babel-loader可以转换ES6或更高版本的语法,可是有些库本身就是用ES5语法书写的,不需要转换,使用babel-loader反而会浪费构建时间 lodash就是这样的一个库,lodash是在ES5之前出现的库,使用的是ES3语法 通过module.rule.exclude或module.rule.include,排除或仅包含需要应用loader的场景

js
module.exports = {
-    module: {
-        rules: [
-            {
-                test: /\.js$/,
-                exclude: /lodash/,
-                use: "babel-loader"
-            }
-        ]
-    }
-}
+    noParse: /jquery/,
+  },
+};
+

1.2 优化 loader 性能

  1. 进一步限制 loader 的应用范围。对于某些库,不使用 loader

例如:babel-loader 可以转换 ES6 或更高版本的语法,可是有些库本身就是用 ES5 语法书写的,不需要转换,使用 babel-loader 反而会浪费构建时间 lodash 就是这样的一个库,lodash 是在 ES5 之前出现的库,使用的是 ES3 语法 通过 module.rule.exclude 或 module.rule.include,排除或仅包含需要应用 loader 的场景

js
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        exclude: /lodash/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
 
module.exports = {
-    module: {
-        rules: [
-            {
-                test: /\.js$/,
-                exclude: /lodash/,
-                use: "babel-loader"
-            }
-        ]
-    }
-}
-

如果暴力一点,甚至可以排除掉node_modules目录中的模块,或仅转换src目录的模块

js
module.exports = {
-    module: {
-        rules: [
-            {
-                test: /\.js$/,
-                exclude: /node_modules/,
-                //或
-                // include: /src/,
-                use: "babel-loader"
-            }
-        ]
-    }
-}
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        exclude: /lodash/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
+

如果暴力一点,甚至可以排除掉 node_modules 目录中的模块,或仅转换 src 目录的模块

js
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        exclude: /node_modules/,
+        //或
+        // include: /src/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
 
module.exports = {
-    module: {
-        rules: [
-            {
-                test: /\.js$/,
-                exclude: /node_modules/,
-                //或
-                // include: /src/,
-                use: "babel-loader"
-            }
-        ]
-    }
-}
-

这种做法是对loader的范围进行进一步的限制,和noParse不冲突

  1. 缓存loader的结果

我们可以基于一种假设:如果某个文件内容不变,经过相同的loader解析后,解析后的结果也不变 于是,可以将loader的解析结果保存下来,让后续的解析直接使用保存的结果 cache-loader可以实现这样的功能:第一次打包会慢,因为有缓存的过程,以后就快了

js
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        exclude: /node_modules/,
+        //或
+        // include: /src/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
+

这种做法是对 loader 的范围进行进一步的限制,和 noParse 不冲突

  1. 缓存 loader 的结果

我们可以基于一种假设:如果某个文件内容不变,经过相同的 loader 解析后,解析后的结果也不变 于是,可以将 loader 的解析结果保存下来,让后续的解析直接使用保存的结果 cache-loader 可以实现这样的功能:第一次打包会慢,因为有缓存的过程,以后就快了

js
module.exports = {
   module: {
     rules: [
       {
         test: /\.js$/,
-        use: ['cache-loader', ...loaders]
+        use: ["cache-loader", ...loaders],
       },
     ],
   },
@@ -87,12 +87,12 @@
     rules: [
       {
         test: /\.js$/,
-        use: ['cache-loader', ...loaders]
+        use: ["cache-loader", ...loaders],
       },
     ],
   },
 };
-

有趣的是,cache-loader放到最前面,却能够决定后续的loader是否运行。实际上,loader的运行过程中,还包含一个过程,即pitch

cache-loader还可以实现各自自定义的配置,具体方式见文档

  1. 为loader的运行开启多线程

thread-loader会开启一个线程池,线程池中包含适量的线程

它会把后续的loader放到线程池的线程中运行,以提高构建效率。由于后续的loader会放到新的线程中,所以,后续的loader不能:

  • 使用 webpack api 生成文件
  • 无法使用自定义的 plugin api
  • 无法访问 webpack options

在实际的开发中,可以进行测试,来决定thread-loader放到什么位置

特别注意,开启和管理线程需要消耗时间,在小型项目中使用thread-loader反而会增加构建时间

HappyPack:受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。

HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了

js
module: {
+

有趣的是,cache-loader 放到最前面,却能够决定后续的 loader 是否运行。实际上,loader 的运行过程中,还包含一个过程,即 pitch

cache-loader 还可以实现各自自定义的配置,具体方式见文档

  1. 为 loader 的运行开启多线程

thread-loader 会开启一个线程池,线程池中包含适量的线程

它会把后续的 loader 放到线程池的线程中运行,以提高构建效率。由于后续的 loader 会放到新的线程中,所以,后续的 loader 不能:

  • 使用 webpack api 生成文件
  • 无法使用自定义的 plugin api
  • 无法访问 webpack options

在实际的开发中,可以进行测试,来决定 thread-loader 放到什么位置

特别注意,开启和管理线程需要消耗时间,在小型项目中使用 thread-loader 反而会增加构建时间

HappyPack:受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。

HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了

js
module: {
   loaders: [
     {
       test: /\.js$/,
@@ -130,35 +130,37 @@
     threads: 4
   })
 ]
-

1.3 热替换 HMR

热替换并不能降低构建时间(可能还会稍微增加),但可以降低代码改动到效果呈现的时间 当使用webpack-dev-server时,考虑代码改动到效果呈现的过程

原理

  1. 更改配置
js
module.exports = {
-  devServer:{
-    hot:true // 开启HMR
+

1.3 热替换 HMR

热替换并不能降低构建时间(可能还会稍微增加),但可以降低代码改动到效果呈现的时间 当使用 webpack-dev-server 时,考虑代码改动到效果呈现的过程

原理

  1. 更改配置
js
module.exports = {
+  devServer: {
+    hot: true, // 开启HMR
   },
-  plugins:[ 
+  plugins: [
     // 可选
-    new webpack.HotModuleReplacementPlugin()
-  ]
-}
+    new webpack.HotModuleReplacementPlugin(),
+  ],
+};
 
module.exports = {
-  devServer:{
-    hot:true // 开启HMR
+  devServer: {
+    hot: true, // 开启HMR
   },
-  plugins:[ 
+  plugins: [
     // 可选
-    new webpack.HotModuleReplacementPlugin()
-  ]
-}
+    new webpack.HotModuleReplacementPlugin(),
+  ],
+};
 
  1. 更改代码
js
// index.js
 
-if(module.hot){ // 是否开启了热更新
-  module.hot.accept() // 接受热更新
+if (module.hot) {
+  // 是否开启了热更新
+  module.hot.accept(); // 接受热更新
 }
 
// index.js
 
-if(module.hot){ // 是否开启了热更新
-  module.hot.accept() // 接受热更新
+if (module.hot) {
+  // 是否开启了热更新
+  module.hot.accept(); // 接受热更新
 }
-

首先,这段代码会参与最终运行!当开启了热更新后,webpack-dev-server会向打包结果中注入module.hot属性。默认情况下,webpack-dev-server不管是否开启了热更新,当重新打包后,都会调用location.reload刷新页面

但如果运行了module.hot.accept(),将改变这一行为module.hot.accept()的作用是让webpack-dev-server通过socket管道,把服务器更新的内容发送到浏览器

然后,将结果交给插件HotModuleReplacementPlugin注入的代码执行 插件HotModuleReplacementPlugin会根据覆盖原始代码,然后让代码重新执行 所以,热替换发生在代码运行期

样式热替换

对于样式也是可以使用热替换的,但需要使用style-loader

因为热替换发生时,HotModuleReplacementPlugin只会简单的重新运行模块代码

因此style-loader的代码一运行,就会重新设置style元素中的样式

mini-css-extract-plugin,由于它生成文件是在构建期间,运行期间并会也无法改动文件,因此它对于热替换是无效的

思考:webpack的热更新是如何做到的?说明其原理?

webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

原理:

首先要知道server端和client端都做了处理工作

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

1.4 其他提升构建性能

  1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码
  2. 通过 externals 配置来提取常⽤库
  3. 利⽤ DllPluginDllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的npm包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
  4. 使⽤ Happypack 实现多线程加速编译
  5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度
  6. 使⽤ Tree-shakingScope Hoisting 来剔除多余代码

思考:如何利用webpack来优化前端性能?(提高性能和体验)

用webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运行快速高效。

  • 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用webpack的UglifyJsPlugin和ParallelUglifyPlugin来压缩JS文件, 利用cssnano(css-loader?minimize)来压缩css
  • 利用CDN加速。在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用webpack对于output参数和各loader的publicPath参数来修改资源路径
  • 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数--optimize-minimize来实现
  • 提取公共代码。

思考:如何提高webpack的构建速度?

  1. 多入口情况下,使用CommonsChunkPlugin来提取公共代码
  2. 通过externals配置来提取常用库
  3. 利用DllPlugin和DllReferencePlugin预编译资源模块 通过DllPlugin来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过DllReferencePlugin将预编译的模块加载进来。
  4. 使用Happypack 实现多线程加速编译
  5. 使用webpack-uglify-parallel来提升uglifyPlugin的压缩速度。 原理上webpack-uglify-parallel采用了多核并行压缩来提升压缩速度
  6. 使用Tree-shaking和Scope Hoisting来剔除多余代码

2. 传输性能

TIP

传输性能是指,打包后的JS代码传输到浏览器经过的时间,在优化传输性能时要考虑到:

  1. 总传输量:所有需要传输的JS文件的内容加起来,就是总传输量,重复代码越少,总传输量越少
  2. 文件数量:当访问页面时,需要传输的JS文件数量,文件数量越多,http请求越多,响应速度越慢
  3. 浏览器缓存:JS文件会被浏览器缓存,被缓存的文件不会再进行传输

2.1 手动分包(极大提升构建性能)

默认情况下,vue-cli会利用webpacksrc目录中的所有代码打包成一个bundle

这样就导致访问一个页面时,需要加载所有页面的js代码

我们可以利用webpack对动态import的支持,从而达到把不同页面的代码打包到不同文件中

js
// routes
+

首先,这段代码会参与最终运行!当开启了热更新后,webpack-dev-server会向打包结果中注入module.hot属性。默认情况下,webpack-dev-server不管是否开启了热更新,当重新打包后,都会调用location.reload刷新页面

但如果运行了module.hot.accept(),将改变这一行为module.hot.accept()的作用是让webpack-dev-server通过 socket 管道,把服务器更新的内容发送到浏览器

然后,将结果交给插件HotModuleReplacementPlugin注入的代码执行 插件HotModuleReplacementPlugin会根据覆盖原始代码,然后让代码重新执行 所以,热替换发生在代码运行期

样式热替换

对于样式也是可以使用热替换的,但需要使用style-loader

因为热替换发生时,HotModuleReplacementPlugin只会简单的重新运行模块代码

因此style-loader的代码一运行,就会重新设置 style 元素中的样式

mini-css-extract-plugin,由于它生成文件是在构建期间,运行期间并会也无法改动文件,因此它对于热替换是无效的

思考:webpack 的热更新是如何做到的?说明其原理?

webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

原理:

首先要知道 server 端和 client 端都做了处理工作

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API 对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了 devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

1.4 其他提升构建性能

  1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码
  2. 通过 externals 配置来提取常⽤库
  3. 利⽤ DllPluginDllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的 npm 包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
  4. 使⽤ Happypack 实现多线程加速编译
  5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度
  6. 使⽤ Tree-shakingScope Hoisting 来剔除多余代码

思考:如何利用 webpack 来优化前端性能?(提高性能和体验)

用 webpack 优化前端性能是指优化 webpack 的输出结果,让打包的最终结果在浏览器运行快速高效。

  • 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用 webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS 文件, 利用 cssnano(css-loader?minimize)来压缩 css
  • 利用 CDN 加速。在构建过程中,将引用的静态资源路径修改为 CDN 上对应的路径。可以利用 webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径
  • 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动 webpack 时追加参数--optimize-minimize 来实现
  • 提取公共代码。

思考:如何提高 webpack 的构建速度?

  1. 多入口情况下,使用 CommonsChunkPlugin 来提取公共代码
  2. 通过 externals 配置来提取常用库
  3. 利用 DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引用但是绝对不会修改的 npm 包来进行预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
  4. 使用 Happypack 实现多线程加速编译
  5. 使用 webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采用了多核并行压缩来提升压缩速度
  6. 使用 Tree-shaking 和 Scope Hoisting 来剔除多余代码

2. 传输性能

TIP

传输性能是指,打包后的 JS 代码传输到浏览器经过的时间,在优化传输性能时要考虑到:

  1. 总传输量:所有需要传输的 JS 文件的内容加起来,就是总传输量,重复代码越少,总传输量越少
  2. 文件数量:当访问页面时,需要传输的 JS 文件数量,文件数量越多,http 请求越多,响应速度越慢
  3. 浏览器缓存:JS 文件会被浏览器缓存,被缓存的文件不会再进行传输

2.1 手动分包(极大提升构建性能)

默认情况下,vue-cli会利用webpacksrc目录中的所有代码打包成一个bundle

这样就导致访问一个页面时,需要加载所有页面的 js 代码

我们可以利用 webpack 对动态 import 的支持,从而达到把不同页面的代码打包到不同文件中

js
// routes
 export default [
   {
     name: "Home",
@@ -168,8 +170,8 @@
   {
     name: "About",
     path: "/about",
-    component: () => import(/* webpackChunkName: "about" */"@/views/About"),
-  }
+    component: () => import(/* webpackChunkName: "about" */ "@/views/About"),
+  },
 ];
 
// routes
 export default [
@@ -181,100 +183,110 @@
   {
     name: "About",
     path: "/about",
-    component: () => import(/* webpackChunkName: "about" */"@/views/About"),
-  }
+    component: () => import(/* webpackChunkName: "about" */ "@/views/About"),
+  },
 ];
-

什么是分包:将一个整体的代码,分布到不同的打包文件中

什么时候要分包?

  • 多个chunk引入了公共模块
  • 公共模块体积较大或较少的变动

基本原理

手动分包的总体思路是:

  1. 先单独的打包公共模块

公共模块会被打包成为动态链接库(dll Dynamic Link Library),并生成资源清单

  1. 根据入口模块进行正常打包

打包时,如果发现模块中使用了资源清单中描述的模块,则不会形成下面的代码结构

js
//源码,入口文件index.js
-import $ from "jquery"
-import _ from "lodash"
+

什么是分包:将一个整体的代码,分布到不同的打包文件中

什么时候要分包?

  • 多个 chunk 引入了公共模块
  • 公共模块体积较大或较少的变动

基本原理

手动分包的总体思路是:

  1. 先单独的打包公共模块

公共模块会被打包成为动态链接库(dll Dynamic Link Library),并生成资源清单

  1. 根据入口模块进行正常打包

打包时,如果发现模块中使用了资源清单中描述的模块,则不会形成下面的代码结构

js
//源码,入口文件index.js
+import $ from "jquery";
+import _ from "lodash";
 _.isArray($(".red"));
 
//源码,入口文件index.js
-import $ from "jquery"
-import _ from "lodash"
+import $ from "jquery";
+import _ from "lodash";
 _.isArray($(".red"));
-

由于资源清单中包含jquery和lodash两个模块,因此打包结果的大致格式是:

js
(function(modules){
+

由于资源清单中包含 jquery 和 lodash 两个模块,因此打包结果的大致格式是:

js
(function (modules) {
   //...
 })({
   // index.js文件的打包结果并没有变化
-  "./src/index.js":
-  function(module, exports, __webpack_require__){
-    var $ = __webpack_require__("./node_modules/jquery/index.js")
-    var _ = __webpack_require__("./node_modules/lodash/index.js")
+  "./src/index.js": function (module, exports, __webpack_require__) {
+    var $ = __webpack_require__("./node_modules/jquery/index.js");
+    var _ = __webpack_require__("./node_modules/lodash/index.js");
     _.isArray($(".red"));
   },
   // 由于资源清单中存在,jquery的代码并不会出现在这里
-  "./node_modules/jquery/index.js":
-  function(module, exports, __webpack_require__){
-    module.exports = jquery;// 直接导出资源清单的名字
+  "./node_modules/jquery/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
+    module.exports = jquery; // 直接导出资源清单的名字
   },
   // 由于资源清单中存在,lodash的代码并不会出现在这里
-  "./node_modules/lodash/index.js":
-  function(module, exports, __webpack_require__){
+  "./node_modules/lodash/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
     module.exports = lodash;
-  }
-})
-
(function(modules){
+  },
+});
+
(function (modules) {
   //...
 })({
   // index.js文件的打包结果并没有变化
-  "./src/index.js":
-  function(module, exports, __webpack_require__){
-    var $ = __webpack_require__("./node_modules/jquery/index.js")
-    var _ = __webpack_require__("./node_modules/lodash/index.js")
+  "./src/index.js": function (module, exports, __webpack_require__) {
+    var $ = __webpack_require__("./node_modules/jquery/index.js");
+    var _ = __webpack_require__("./node_modules/lodash/index.js");
     _.isArray($(".red"));
   },
   // 由于资源清单中存在,jquery的代码并不会出现在这里
-  "./node_modules/jquery/index.js":
-  function(module, exports, __webpack_require__){
-    module.exports = jquery;// 直接导出资源清单的名字
+  "./node_modules/jquery/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
+    module.exports = jquery; // 直接导出资源清单的名字
   },
   // 由于资源清单中存在,lodash的代码并不会出现在这里
-  "./node_modules/lodash/index.js":
-  function(module, exports, __webpack_require__){
+  "./node_modules/lodash/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
     module.exports = lodash;
-  }
-})
-
  1. 打包公共模块

打包公共模块是一个独立的打包过程

  1. 单独打包公共模块,暴露变量名 . npm run dll
js
// webpack.dll.config.js
+  },
+});
+
  1. 打包公共模块

打包公共模块是一个独立的打包过程

  1. 单独打包公共模块,暴露变量名 . npm run dll
js
// webpack.dll.config.js
 module.exports = {
   mode: "production",
   entry: {
-    jquery: ["jquery"],//数组
-    lodash: ["lodash"]
+    jquery: ["jquery"], //数组
+    lodash: ["lodash"],
   },
   output: {
     filename: "dll/[name].js",
-    library: "[name]"// 每个bundle暴露的全局变量名
-  }
+    library: "[name]", // 每个bundle暴露的全局变量名
+  },
 };
 
// webpack.dll.config.js
 module.exports = {
   mode: "production",
   entry: {
-    jquery: ["jquery"],//数组
-    lodash: ["lodash"]
+    jquery: ["jquery"], //数组
+    lodash: ["lodash"],
   },
   output: {
     filename: "dll/[name].js",
-    library: "[name]"// 每个bundle暴露的全局变量名
-  }
+    library: "[name]", // 每个bundle暴露的全局变量名
+  },
 };
 

利用DllPlugin生成资源清单

js
// webpack.dll.config.js
 module.exports = {
   plugins: [
     new webpack.DllPlugin({
       path: path.resolve(__dirname, "dll", "[name].manifest.json"), //资源清单的保存位置
-      name: "[name]"//资源清单中,暴露的变量名
-    })
-  ]
+      name: "[name]", //资源清单中,暴露的变量名
+    }),
+  ],
 };
 
// webpack.dll.config.js
 module.exports = {
   plugins: [
     new webpack.DllPlugin({
       path: path.resolve(__dirname, "dll", "[name].manifest.json"), //资源清单的保存位置
-      name: "[name]"//资源清单中,暴露的变量名
-    })
-  ]
+      name: "[name]", //资源清单中,暴露的变量名
+    }),
+  ],
 };
 

运行后,即可完成公共模块打包

使用公共模块

  1. 在页面中手动引入公共模块
html
<script src="./dll/jquery.js"></script>
 <script src="./dll/lodash.js"></script>
@@ -283,81 +295,83 @@
 

重新设置clean-webpack-plugin。如果使用了插件clean-webpack-plugin,为了避免它把公共模块清除,需要做出以下配置

js
new CleanWebpackPlugin({
   // 要清除的文件或目录
   // 排除掉dll目录本身和它里面的文件
-  cleanOnceBeforeBuildPatterns: ["**/*", '!dll', '!dll/*']
-})
+  cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"],
+});
 
new CleanWebpackPlugin({
   // 要清除的文件或目录
   // 排除掉dll目录本身和它里面的文件
-  cleanOnceBeforeBuildPatterns: ["**/*", '!dll', '!dll/*']
-})
-

目录和文件的匹配规则使用的是globbing patterns语法

使用DllReferencePlugin控制打包结果

js
module.exports = {
-  plugins:[// 资源清单
+  cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"],
+});
+

目录和文件的匹配规则使用的是globbing patterns语法

使用 DllReferencePlugin 控制打包结果

js
module.exports = {
+  plugins: [
+    // 资源清单
     new webpack.DllReferencePlugin({
-      manifest: require("./dll/jquery.manifest.json")
+      manifest: require("./dll/jquery.manifest.json"),
     }),
     new webpack.DllReferencePlugin({
-      manifest: require("./dll/lodash.manifest.json")
-    })
-  ]
-}
+      manifest: require("./dll/lodash.manifest.json"),
+    }),
+  ],
+};
 
module.exports = {
-  plugins:[// 资源清单
+  plugins: [
+    // 资源清单
     new webpack.DllReferencePlugin({
-      manifest: require("./dll/jquery.manifest.json")
+      manifest: require("./dll/jquery.manifest.json"),
     }),
     new webpack.DllReferencePlugin({
-      manifest: require("./dll/lodash.manifest.json")
-    })
-  ]
-}
-

总结

手动打包的过程:

  1. 开启output.library暴露公共模块
  2. 用DllPlugin创建资源清单
  3. 用DllReferencePlugin使用资源清单

手动打包的注意事项:

  1. 资源清单不参与运行,可以不放到打包目录中
  2. 记得手动引入公共JS,以及避免被删除
  3. 不要对小型的公共JS库使用

优点:

  1. 极大提升自身模块的打包速度
  2. 极大的缩小了自身文件体积
  3. 有利于浏览器缓存第三方库的公共代码

缺点:

  1. 使用非常繁琐
  2. 如果第三方库中包含重复代码,则效果不太理想

详解dllPlugin

DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin的使用方法如下:

js
// 单独配置在一个文件中
+      manifest: require("./dll/lodash.manifest.json"),
+    }),
+  ],
+};
+

总结

手动打包的过程:

  1. 开启 output.library 暴露公共模块
  2. 用 DllPlugin 创建资源清单
  3. 用 DllReferencePlugin 使用资源清单

手动打包的注意事项:

  1. 资源清单不参与运行,可以不放到打包目录中
  2. 记得手动引入公共 JS,以及避免被删除
  3. 不要对小型的公共 JS 库使用

优点:

  1. 极大提升自身模块的打包速度
  2. 极大的缩小了自身文件体积
  3. 有利于浏览器缓存第三方库的公共代码

缺点:

  1. 使用非常繁琐
  2. 如果第三方库中包含重复代码,则效果不太理想

详解 dllPlugin

DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin 的使用方法如下:

js
// 单独配置在一个文件中
 // webpack.dll.conf.js
-const path = require('path')
-const webpack = require('webpack')
+const path = require("path");
+const webpack = require("webpack");
 module.exports = {
   entry: {
     // 想统一打包的类库
-    vendor: ['react']
+    vendor: ["react"],
   },
   output: {
-    path: path.join(__dirname, 'dist'),
-    filename: '[name].dll.js',
-    library: '[name]-[hash]'
+    path: path.join(__dirname, "dist"),
+    filename: "[name].dll.js",
+    library: "[name]-[hash]",
   },
   plugins: [
     new webpack.DllPlugin({
       // name 必须和 output.library 一致
-      name: '[name]-[hash]',
+      name: "[name]-[hash]",
       // 该属性需要与 DllReferencePlugin 中一致
       context: __dirname,
-      path: path.join(__dirname, 'dist', '[name]-manifest.json')
-    })
-  ]
-}
+      path: path.join(__dirname, "dist", "[name]-manifest.json"),
+    }),
+  ],
+};
 
// 单独配置在一个文件中
 // webpack.dll.conf.js
-const path = require('path')
-const webpack = require('webpack')
+const path = require("path");
+const webpack = require("webpack");
 module.exports = {
   entry: {
     // 想统一打包的类库
-    vendor: ['react']
+    vendor: ["react"],
   },
   output: {
-    path: path.join(__dirname, 'dist'),
-    filename: '[name].dll.js',
-    library: '[name]-[hash]'
+    path: path.join(__dirname, "dist"),
+    filename: "[name].dll.js",
+    library: "[name]-[hash]",
   },
   plugins: [
     new webpack.DllPlugin({
       // name 必须和 output.library 一致
-      name: '[name]-[hash]',
+      name: "[name]-[hash]",
       // 该属性需要与 DllReferencePlugin 中一致
       context: __dirname,
-      path: path.join(__dirname, 'dist', '[name]-manifest.json')
-    })
-  ]
-}
+      path: path.join(__dirname, "dist", "[name]-manifest.json"),
+    }),
+  ],
+};
 

然后需要执行这个配置文件生成依赖文件,接下来需要使用 DllReferencePlugin 将依赖文件引入项目中

js
// webpack.conf.js
 module.exports = {
   // ...省略其他配置
@@ -365,10 +379,10 @@
     new webpack.DllReferencePlugin({
       context: __dirname,
       // manifest 就是之前打包出来的 json 文件
-      manifest: require('./dist/vendor-manifest.json'),
-    })
-  ]
-}
+      manifest: require("./dist/vendor-manifest.json"),
+    }),
+  ],
+};
 
// webpack.conf.js
 module.exports = {
   // ...省略其他配置
@@ -376,211 +390,219 @@
     new webpack.DllReferencePlugin({
       context: __dirname,
       // manifest 就是之前打包出来的 json 文件
-      manifest: require('./dist/vendor-manifest.json'),
-    })
-  ]
-}
-

可以通过一些小的优化点来加快打包速度

  • resolve.extensions:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面
  • resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径
  • module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助

2.2 自动分包(会降低构建效率,开发效率提升,新的模块不需要手动处理了)

  1. 基本原理

不同于手动分包,自动分包是从实际的角度出发,从一个更加宏观的角度来控制分包,而一般不对具体哪个包要分出去进行控制

因此使用自动分包,不仅非常方便,而且更加贴合实际的开发需要

要控制自动分包,关键是要配置一个合理的分包策略

有了分包策略之后,不需要额外安装任何插件,webpack会自动的按照策略进行分包

实际上,webpack在内部是使用SplitChunksPlugin进行分包的 过去有一个库CommonsChunkPlugin也可以实现分包,不过由于该库某些地方并不完善,到了webpack4之后,已被SplitChunksPlugin取代

从分包流程中至少可以看出以下几点:

  • 分包策略至关重要,它决定了如何分包
  • 分包时,webpack开启了一个新的chunk,对分离的模块进行打包
  • 打包结果中,公共的部分被提取出来形成了一个单独的文件,它是新chunk的产物
  1. 分包策略的基本配置

webpack提供了optimization配置项,用于配置一些优化信息

其中splitChunks是分包策略的配置

js
module.exports = {
+      manifest: require("./dist/vendor-manifest.json"),
+    }),
+  ],
+};
+

可以通过一些小的优化点来加快打包速度

  • resolve.extensions:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面
  • resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径
  • module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助

2.2 自动分包(会降低构建效率,开发效率提升,新的模块不需要手动处理了)

  1. 基本原理

不同于手动分包,自动分包是从实际的角度出发,从一个更加宏观的角度来控制分包,而一般不对具体哪个包要分出去进行控制

因此使用自动分包,不仅非常方便,而且更加贴合实际的开发需要

要控制自动分包,关键是要配置一个合理的分包策略

有了分包策略之后,不需要额外安装任何插件,webpack 会自动的按照策略进行分包

实际上,webpack 在内部是使用 SplitChunksPlugin 进行分包的 过去有一个库 CommonsChunkPlugin 也可以实现分包,不过由于该库某些地方并不完善,到了 webpack4 之后,已被 SplitChunksPlugin 取代

从分包流程中至少可以看出以下几点:

  • 分包策略至关重要,它决定了如何分包
  • 分包时,webpack 开启了一个新的 chunk,对分离的模块进行打包
  • 打包结果中,公共的部分被提取出来形成了一个单独的文件,它是新 chunk 的产物
  1. 分包策略的基本配置

webpack 提供了 optimization 配置项,用于配置一些优化信息

其中 splitChunks 是分包策略的配置

js
module.exports = {
   optimization: {
     splitChunks: {
       // 分包策略
-    }
-  }
-}
+    },
+  },
+};
 
module.exports = {
   optimization: {
     splitChunks: {
       // 分包策略
-    }
-  }
-}
-

事实上,分包策略有其默认的配置,我们只需要轻微的改动,即可应对大部分分包场景

chunks

该配置项用于配置需要应用分包策略的chunk

我们知道,分包是从已有的chunk中分离出新的chunk,那么哪些chunk需要分离呢

chunks有三个取值,分别是:

  • all: 对于所有的chunk都要应用分包策略
  • async:【默认】仅针对异步chunk应用分包策略
  • initial:仅针对普通chunk应用分包策略

所以,你只需要配置chunks为all即可

maxSize

该配置可以控制包的最大字节数

如果某个包(包括分出来的包)超过了该值,则webpack会尽可能的将其分离成多个包

但是不要忽略的是,分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的,因此,尽管使用了这个配置,完全有可能某个包还是会超过这个体积

另外,该配置看上去很美妙,实际意义其实不大

因为分包的目的是提取大量的公共代码,从而减少总体积和充分利用浏览器缓存

虽然该配置可以把一些包进行再切分,但是实际的总体积和传输量并没有发生变化

如果要进一步减少公共模块的体积,只能是压缩和tree shaking

  1. 分包策略的其他配置

如果不想使用其他配置的默认值,可以手动进行配置:

  • automaticNameDelimiter:新chunk名称的分隔符,默认值~

  • minChunks:一个模块被多少个chunk使用时,才会进行分包,默认值1。如果我自己写一个文件,默认也不分包,因为自己写的那个太小,没达到拆分的条件,所以要配合minSize使用。

  • minSize:当分包达到多少字节后才允许被真正的拆分,默认值30000

  1. 缓存组

之前配置的分包策略是全局的

而实际上,分包策略是基于缓存组的

每个缓存组提供一套独有的策略,webpack按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包

默认情况下,webpack提供了两个缓存组:

js
module.exports = {
-  optimization:{
+    },
+  },
+};
+

事实上,分包策略有其默认的配置,我们只需要轻微的改动,即可应对大部分分包场景

chunks

该配置项用于配置需要应用分包策略的 chunk

我们知道,分包是从已有的 chunk 中分离出新的 chunk,那么哪些 chunk 需要分离呢

chunks 有三个取值,分别是:

  • all: 对于所有的 chunk 都要应用分包策略
  • async:【默认】仅针对异步 chunk 应用分包策略
  • initial:仅针对普通 chunk 应用分包策略

所以,你只需要配置 chunks 为 all 即可

maxSize

该配置可以控制包的最大字节数

如果某个包(包括分出来的包)超过了该值,则 webpack 会尽可能的将其分离成多个包

但是不要忽略的是,分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的,因此,尽管使用了这个配置,完全有可能某个包还是会超过这个体积

另外,该配置看上去很美妙,实际意义其实不大

因为分包的目的是提取大量的公共代码,从而减少总体积和充分利用浏览器缓存

虽然该配置可以把一些包进行再切分,但是实际的总体积和传输量并没有发生变化

如果要进一步减少公共模块的体积,只能是压缩和 tree shaking

  1. 分包策略的其他配置

如果不想使用其他配置的默认值,可以手动进行配置:

  • automaticNameDelimiter:新 chunk 名称的分隔符,默认值~

  • minChunks:一个模块被多少个 chunk 使用时,才会进行分包,默认值 1。如果我自己写一个文件,默认也不分包,因为自己写的那个太小,没达到拆分的条件,所以要配合 minSize 使用。

  • minSize:当分包达到多少字节后才允许被真正的拆分,默认值 30000

  1. 缓存组

之前配置的分包策略是全局的

而实际上,分包策略是基于缓存组的

每个缓存组提供一套独有的策略,webpack 按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包

默认情况下,webpack 提供了两个缓存组:

js
module.exports = {
+  optimization: {
     splitChunks: {
       //全局配置
       cacheGroups: {
         // 属性名是缓存组名称,会影响到分包的chunk名
         // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
-        vendors: { 
+        vendors: {
           test: /[\\/]node_modules[\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
-          priority: -10 // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
+          priority: -10, // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
         },
         default: {
-          minChunks: 2,  // 覆盖全局配置,将最小chunk引用数改为2
+          minChunks: 2, // 覆盖全局配置,将最小chunk引用数改为2
           priority: -20, // 优先级
-          reuseExistingChunk: true // 重用已经被分离出去的chunk
-        }
-      }
-    }
-  }
-}
+          reuseExistingChunk: true, // 重用已经被分离出去的chunk
+        },
+      },
+    },
+  },
+};
 
module.exports = {
-  optimization:{
+  optimization: {
     splitChunks: {
       //全局配置
       cacheGroups: {
         // 属性名是缓存组名称,会影响到分包的chunk名
         // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
-        vendors: { 
+        vendors: {
           test: /[\\/]node_modules[\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
-          priority: -10 // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
+          priority: -10, // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
         },
         default: {
-          minChunks: 2,  // 覆盖全局配置,将最小chunk引用数改为2
+          minChunks: 2, // 覆盖全局配置,将最小chunk引用数改为2
           priority: -20, // 优先级
-          reuseExistingChunk: true // 重用已经被分离出去的chunk
-        }
-      }
-    }
-  }
-}
+          reuseExistingChunk: true, // 重用已经被分离出去的chunk
+        },
+      },
+    },
+  },
+};
 

很多时候,缓存组对于我们来说没什么意义,因为默认的缓存组就已经够用了

但是我们同样可以利用缓存组来完成一些事情,比如对公共样式的抽离

js
module.exports = {
   optimization: {
     splitChunks: {
       chunks: "all",
       cacheGroups: {
-        styles: {// 样式抽离
+        styles: {
+          // 样式抽离
           test: /\.css$/, // 匹配样式模块
           minSize: 0, // 覆盖默认的最小尺寸,这里仅仅是作为测试
-          minChunks: 2 // 覆盖默认的最小chunk引用数
-        }
-      }
-    }
+          minChunks: 2, // 覆盖默认的最小chunk引用数
+        },
+      },
+    },
   },
   module: {
-    rules: [{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }]
+    rules: [
+      { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
+    ],
   },
   plugins: [
     new CleanWebpackPlugin(),
     new HtmlWebpackPlugin({
       template: "./public/index.html",
-      chunks: ["index"]
+      chunks: ["index"],
     }),
     new MiniCssExtractPlugin({
       filename: "[name].[hash:5].css",
       // chunkFilename是配置来自于分割chunk的文件名
-      chunkFilename: "common.[hash:5].css" 
-    })
-  ]
-}
+      chunkFilename: "common.[hash:5].css",
+    }),
+  ],
+};
 
module.exports = {
   optimization: {
     splitChunks: {
       chunks: "all",
       cacheGroups: {
-        styles: {// 样式抽离
+        styles: {
+          // 样式抽离
           test: /\.css$/, // 匹配样式模块
           minSize: 0, // 覆盖默认的最小尺寸,这里仅仅是作为测试
-          minChunks: 2 // 覆盖默认的最小chunk引用数
-        }
-      }
-    }
+          minChunks: 2, // 覆盖默认的最小chunk引用数
+        },
+      },
+    },
   },
   module: {
-    rules: [{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }]
+    rules: [
+      { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
+    ],
   },
   plugins: [
     new CleanWebpackPlugin(),
     new HtmlWebpackPlugin({
       template: "./public/index.html",
-      chunks: ["index"]
+      chunks: ["index"],
     }),
     new MiniCssExtractPlugin({
       filename: "[name].[hash:5].css",
       // chunkFilename是配置来自于分割chunk的文件名
-      chunkFilename: "common.[hash:5].css" 
-    })
-  ]
-}
-
  1. 配合多页应用

虽然现在单页应用是主流,但免不了还是会遇到多页应用

由于在多页应用中需要为每个html页面指定需要的chunk,否则都会引入进去,这就造成了问题

js
new HtmlWebpackPlugin({
+      chunkFilename: "common.[hash:5].css",
+    }),
+  ],
+};
+
  1. 配合多页应用

虽然现在单页应用是主流,但免不了还是会遇到多页应用

由于在多页应用中需要为每个 html 页面指定需要的 chunk,否则都会引入进去,这就造成了问题

js
new HtmlWebpackPlugin({
   template: "./public/index.html",
-  chunks: ["index~other", "vendors~index~other", "index"]
-})
+  chunks: ["index~other", "vendors~index~other", "index"],
+});
 
new HtmlWebpackPlugin({
   template: "./public/index.html",
-  chunks: ["index~other", "vendors~index~other", "index"]
-})
-

我们必须手动的指定被分离出去的chunk名称,这不是一种好办法

幸好html-webpack-plugin的新版本中解决了这一问题

shell
npm i -D html-webpack-plugin@next
+  chunks: ["index~other", "vendors~index~other", "index"],
+});
+

我们必须手动的指定被分离出去的 chunk 名称,这不是一种好办法

幸好html-webpack-plugin的新版本中解决了这一问题

shell
npm i -D html-webpack-plugin@next
 
npm i -D html-webpack-plugin@next
 

做出以下配置即可:

js
new HtmlWebpackPlugin({
   template: "./public/index.html",
-  chunks: ["index"]
-})
+  chunks: ["index"],
+});
 
new HtmlWebpackPlugin({
   template: "./public/index.html",
-  chunks: ["index"]
-})
-

它会自动的找到被index分离出去的chunk,并完成引用

目前这个版本仍处于测试解决,还未正式发布

  1. 原理

自动分包的原理其实并不复杂,主要经过以下步骤:

  • 检查每个chunk编译的结果
  • 根据分包策略,找到那些满足策略的模块
  • 根据分包策略,生成新的chunk打包这些模块(代码有所变化)
  • 把打包出去的模块从原始包中移除,并修正原始包代码

在代码层面,有以下变动

  1. 分包的代码中,加入一个全局变量webpackJsonp,类型为数组,其中包含公共模块的代码
  2. 原始包的代码中,使用数组中的公共代码

2.3 代码压缩

单模块体积优化

  1. 为什么要进行代码压缩: 减少代码体积;破坏代码的可读性,提升破解成本;
  2. 什么时候要进行代码压缩: 生产环境
  3. 使用什么压缩工具: 目前最流行的代码压缩工具主要有两个:UglifyJs和Terser

UglifyJs是一个传统的代码压缩工具,已存在多年,曾经是前端应用的必备工具,但由于它不支持ES6语法,所以目前的流行度已有所下降。

Terser是一个新起的代码压缩工具,支持ES6+语法,因此被很多构建工具内置使用。webpack安装后会内置Terser,当启用生产环境后即可用其进行代码压缩。

因此,我们选择Terser

关于副作用 side effect

副作用:函数运行过程中,可能会对外部环境造成影响的功能

如果函数中包含以下代码,该函数叫做副作用函数:

  • 异步代码
  • localStorage
  • 对外部数据的修改

如果一个函数没有副作用,同时,函数的返回结果仅依赖参数,则该函数叫做纯函数(pure function)

纯函数非常有利于压缩优化。可以手动指定那些是纯函数:pure_funcs:['Math.random']

Terser

在Terser的官网可尝试它的压缩效果

Terser官网:https://terser.org/

webpack+Terser

webpack自动集成了Terser

如果你想更改、添加压缩工具,又或者是想对Terser进行配置,使用下面的webpack配置即可

js
const TerserPlugin = require('terser-webpack-plugin');
-const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+  chunks: ["index"],
+});
+

它会自动的找到被 index 分离出去的 chunk,并完成引用

目前这个版本仍处于测试解决,还未正式发布

  1. 原理

自动分包的原理其实并不复杂,主要经过以下步骤:

  • 检查每个 chunk 编译的结果
  • 根据分包策略,找到那些满足策略的模块
  • 根据分包策略,生成新的 chunk 打包这些模块(代码有所变化)
  • 把打包出去的模块从原始包中移除,并修正原始包代码

在代码层面,有以下变动

  1. 分包的代码中,加入一个全局变量 webpackJsonp,类型为数组,其中包含公共模块的代码
  2. 原始包的代码中,使用数组中的公共代码

2.3 代码压缩

单模块体积优化

  1. 为什么要进行代码压缩: 减少代码体积;破坏代码的可读性,提升破解成本;
  2. 什么时候要进行代码压缩: 生产环境
  3. 使用什么压缩工具: 目前最流行的代码压缩工具主要有两个:UglifyJs 和 Terser

UglifyJs 是一个传统的代码压缩工具,已存在多年,曾经是前端应用的必备工具,但由于它不支持 ES6 语法,所以目前的流行度已有所下降。

Terser 是一个新起的代码压缩工具,支持 ES6+语法,因此被很多构建工具内置使用。webpack 安装后会内置 Terser,当启用生产环境后即可用其进行代码压缩。

因此,我们选择 Terser

关于副作用 side effect

副作用:函数运行过程中,可能会对外部环境造成影响的功能

如果函数中包含以下代码,该函数叫做副作用函数:

  • 异步代码
  • localStorage
  • 对外部数据的修改

如果一个函数没有副作用,同时,函数的返回结果仅依赖参数,则该函数叫做纯函数(pure function)

纯函数非常有利于压缩优化。可以手动指定那些是纯函数:pure_funcs:['Math.random']

Terser

在 Terser 的官网可尝试它的压缩效果

Terser 官网:https://terser.org/

webpack+Terser

webpack 自动集成了 Terser

如果你想更改、添加压缩工具,又或者是想对 Terser 进行配置,使用下面的 webpack 配置即可

js
const TerserPlugin = require("terser-webpack-plugin");
+const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
 module.exports = {
   optimization: {
     minimize: true, // 是否要启用压缩,默认情况下,生产环境会自动开启
-    minimizer: [ // 压缩时使用的插件,可以有多个
-      new TerserPlugin(), 
-      new OptimizeCSSAssetsPlugin()
+    minimizer: [
+      // 压缩时使用的插件,可以有多个
+      new TerserPlugin(),
+      new OptimizeCSSAssetsPlugin(),
     ],
   },
 };
-
const TerserPlugin = require('terser-webpack-plugin');
-const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+
const TerserPlugin = require("terser-webpack-plugin");
+const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
 module.exports = {
   optimization: {
     minimize: true, // 是否要启用压缩,默认情况下,生产环境会自动开启
-    minimizer: [ // 压缩时使用的插件,可以有多个
-      new TerserPlugin(), 
-      new OptimizeCSSAssetsPlugin()
+    minimizer: [
+      // 压缩时使用的插件,可以有多个
+      new TerserPlugin(),
+      new OptimizeCSSAssetsPlugin(),
     ],
   },
 };
-

2.4 tree shaking

压缩可以移除模块内部的无效代码 tree shaking 可以移除模块之间的无效代码

  1. 背景 某些模块导出的代码并不一定会被用到,第三方库就是个典型例子
js
// myMath.js
-export function add(a, b){
-  console.log("add")
-  return a+b;
+

2.4 tree shaking

压缩可以移除模块内部的无效代码 tree shaking 可以移除模块之间的无效代码

  1. 背景 某些模块导出的代码并不一定会被用到,第三方库就是个典型例子
js
// myMath.js
+export function add(a, b) {
+  console.log("add");
+  return a + b;
 }
 
-export function sub(a, b){
-  console.log("sub")
-  return a-b;
+export function sub(a, b) {
+  console.log("sub");
+  return a - b;
 }
 // index.js
-import {add} from "./myMath"
-console.log(add(1,2));
+import { add } from "./myMath";
+console.log(add(1, 2));
 
// myMath.js
-export function add(a, b){
-  console.log("add")
-  return a+b;
+export function add(a, b) {
+  console.log("add");
+  return a + b;
 }
 
-export function sub(a, b){
-  console.log("sub")
-  return a-b;
+export function sub(a, b) {
+  console.log("sub");
+  return a - b;
 }
 // index.js
-import {add} from "./myMath"
-console.log(add(1,2));
-

tree shaking 用于移除掉不会用到的导出

  1. 使用

webpack2开始就支持了tree shaking

只要是生产环境,tree shaking自动开启

  1. 原理

webpack会从入口模块出发寻找依赖关系

当解析一个模块时,webpack会根据ES6的模块导入语句来判断,该模块依赖了另一个模块的哪个导出

webpack之所以选择ES6的模块导入语句,是因为ES6模块有以下特点:commonjs不具备

  • 导入导出语句只能是顶层语句
  • import的模块名只能是字符串常量
  • import绑定的变量是不可变的

这些特征都非常有利于分析出稳定的依赖

在具体分析依赖时,webpack坚持的原则是:保证代码正常运行,然后再尽量tree shaking

所以,如果你依赖的是一个导出的对象,由于JS语言的动态特性,以及webpack还不够智能,为了保证代码正常运行,它不会移除对象中的任何信息

因此,我们在编写代码的时候,尽量:

  • 使用export xxx导出,而不使用export default {xxx}导出。后者会整个导出,但是不一定都需要。
  • 使用import {xxx} from "xxx"导入,而不使用import xxx from "xxx"导入

依赖分析完毕后,webpack会根据每个模块每个导出是否被使用,标记其他导出为dead code,然后交给代码压缩工具处理

代码压缩工具最终移除掉那些dead code代码

  1. 使用第三方库

某些第三方库可能使用的是commonjs的方式导出,比如lodash

又或者没有提供普通的ES6方式导出

对于这些库,tree shaking是无法发挥作用的

因此要寻找这些库的es6版本,好在很多流行但没有使用的ES6的第三方库,都发布了它的ES6版本,比如lodash-es

  1. 作用域分析

tree shaking本身并没有完善的作用域分析,可能导致在一些dead code函数中的依赖仍然会被视为依赖 比如a引用b,b引用了lodash,但是a没有用到b用lodash的导出代码 插件webpack-deep-scope-plugin提供了作用域分析,可解决这些问题

  1. 副作用问题

webpack在tree shaking的使用,有一个原则:一定要保证代码正确运行

在满足该原则的基础上,再来决定如何tree shaking

因此,当webpack无法确定某个模块是否有副作用时,它往往将其视为有副作用

因此,某些情况可能并不是我们所想要的

js
//common.js
-var n  = Math.random();
+import { add } from "./myMath";
+console.log(add(1, 2));
+

tree shaking 用于移除掉不会用到的导出

  1. 使用

webpack2 开始就支持了 tree shaking

只要是生产环境,tree shaking 自动开启

  1. 原理

webpack 会从入口模块出发寻找依赖关系

当解析一个模块时,webpack 会根据 ES6 的模块导入语句来判断,该模块依赖了另一个模块的哪个导出

webpack 之所以选择 ES6 的模块导入语句,是因为 ES6 模块有以下特点:commonjs 不具备

  • 导入导出语句只能是顶层语句
  • import 的模块名只能是字符串常量
  • import 绑定的变量是不可变的

这些特征都非常有利于分析出稳定的依赖

在具体分析依赖时,webpack 坚持的原则是:保证代码正常运行,然后再尽量 tree shaking

所以,如果你依赖的是一个导出的对象,由于 JS 语言的动态特性,以及 webpack 还不够智能,为了保证代码正常运行,它不会移除对象中的任何信息

因此,我们在编写代码的时候,尽量:

  • 使用 export xxx 导出,而不使用 export default {xxx}导出。后者会整个导出,但是不一定都需要。
  • 使用 import {xxx} from "xxx"导入,而不使用 import xxx from "xxx"导入

依赖分析完毕后,webpack 会根据每个模块每个导出是否被使用,标记其他导出为 dead code,然后交给代码压缩工具处理

代码压缩工具最终移除掉那些 dead code 代码

  1. 使用第三方库

某些第三方库可能使用的是 commonjs 的方式导出,比如 lodash

又或者没有提供普通的 ES6 方式导出

对于这些库,tree shaking 是无法发挥作用的

因此要寻找这些库的 es6 版本,好在很多流行但没有使用的 ES6 的第三方库,都发布了它的 ES6 版本,比如 lodash-es

  1. 作用域分析

tree shaking 本身并没有完善的作用域分析,可能导致在一些 dead code 函数中的依赖仍然会被视为依赖 比如 a 引用 b,b 引用了 lodash,但是 a 没有用到 b 用 lodash 的导出代码 插件 webpack-deep-scope-plugin 提供了作用域分析,可解决这些问题

  1. 副作用问题

webpack 在 tree shaking 的使用,有一个原则:一定要保证代码正确运行

在满足该原则的基础上,再来决定如何 tree shaking

因此,当 webpack 无法确定某个模块是否有副作用时,它往往将其视为有副作用

因此,某些情况可能并不是我们所想要的

js
//common.js
+var n = Math.random();
 
 //index.js
-import "./common.js"
+import "./common.js";
 
//common.js
-var n  = Math.random();
+var n = Math.random();
 
 //index.js
-import "./common.js"
-

虽然我们根本没用有common.js的导出,但webpack担心common.js有副作用,如果去掉会影响某些功能

如果要解决该问题,就需要标记该文件是没有副作用的

在package.json中加入sideEffects

json
{
-    "sideEffects": false
+import "./common.js";
+

虽然我们根本没用有 common.js 的导出,但 webpack 担心 common.js 有副作用,如果去掉会影响某些功能

如果要解决该问题,就需要标记该文件是没有副作用的

在 package.json 中加入 sideEffects

json
{
+  "sideEffects": false
 }
 
{
-    "sideEffects": false
+  "sideEffects": false
 }
-

有两种配置方式:

  • false:当前工程中,所有模块都没有副作用。注意,这种写法会影响到某些css文件的导入
  • 数组:设置哪些文件拥有副作用,例如:["!src/common.js"],表示只要不是src/common.js的文件,都有副作用
js
{
+

有两种配置方式:

  • false:当前工程中,所有模块都没有副作用。注意,这种写法会影响到某些 css 文件的导入
  • 数组:设置哪些文件拥有副作用,例如:["!src/common.js"],表示只要不是 src/common.js 的文件,都有副作用
js
{
     "sideEffects": ["!src/common.js"]
 }
 
{
     "sideEffects": ["!src/common.js"]
 }
-

这种方式我们一般不处理,通常是一些第三方库在它们自己的package.json中标注

webpack无法对css完成tree shaking,因为css跟es6没有半毛钱关系。

因此对css的tree shaking需要其他插件完成。例如:purgecss-webpack-plugin。注意:purgecss-webpack-plugin对css module无能为力

2.5 懒加载

可以理解为异步chunk

js
// 异步加载使用import语法
+

这种方式我们一般不处理,通常是一些第三方库在它们自己的 package.json 中标注

webpack 无法对 css 完成 tree shaking,因为 css 跟 es6 没有半毛钱关系。

因此对 css 的 tree shaking 需要其他插件完成。例如:purgecss-webpack-plugin。注意:purgecss-webpack-plugin 对 css module 无能为力

2.5 懒加载

可以理解为异步 chunk

js
// 异步加载使用import语法
 const btn = document.querySelector("button");
 btn.onclick = async function () {
   //动态加载
@@ -588,9 +610,7 @@
   //浏览器会使用JSOP的方式远程去读取一个js模块
   //import()会返回一个promise   (返回结果类似于 * as obj)
   // const { chunk } = await import(/* webpackChunkName:"lodash" */"lodash-es");
-  const {
-    chunk
-  } = await import("./util");// 搞成静态依赖就行 所以加上了util.js
+  const { chunk } = await import("./util"); // 搞成静态依赖就行 所以加上了util.js
   const result = chunk([3, 5, 6, 7, 87], 2);
   console.log(result);
 };
@@ -605,39 +625,37 @@
   //浏览器会使用JSOP的方式远程去读取一个js模块
   //import()会返回一个promise   (返回结果类似于 * as obj)
   // const { chunk } = await import(/* webpackChunkName:"lodash" */"lodash-es");
-  const {
-    chunk
-  } = await import("./util");// 搞成静态依赖就行 所以加上了util.js
+  const { chunk } = await import("./util"); // 搞成静态依赖就行 所以加上了util.js
   const result = chunk([3, 5, 6, 7, 87], 2);
   console.log(result);
 };
 
 // 因为是动态的,所以tree shaking没了
 // 如果想用该咋办?搞成静态依赖就行 所以加上了util.js
-

2.6 gzip

gzip是一种压缩文件的算法

B/S结构中的压缩传输

  • 浏览器告诉服务器支持那些压缩方式。
  • 响应头:什么方式解压->ungzip

优点:传输效率可能得到大幅提升

缺点:服务器的压缩需要时间,客户端的解压需要时间

gzip的原理

gizp压缩是一种http请求优化方式,通过减少文件体积来提高加载速度。html、js、css文件甚至json数据都可以用它压缩,可以减小60%以上的体积。前端配置gzip压缩,并且服务端使用nginx开启gzip,用来减小网络传输的流量大小。

使用webpack进行预压缩

使用compression-webpack-plugin插件对打包结果进行预压缩,可以移除服务器的压缩时间

js
plugins: [
+

2.6 gzip

gzip 是一种压缩文件的算法

B/S 结构中的压缩传输

  • 浏览器告诉服务器支持那些压缩方式。
  • 响应头:什么方式解压->ungzip

优点:传输效率可能得到大幅提升

缺点:服务器的压缩需要时间,客户端的解压需要时间

gzip 的原理

gizp 压缩是一种 http 请求优化方式,通过减少文件体积来提高加载速度。html、js、css 文件甚至 json 数据都可以用它压缩,可以减小 60%以上的体积。前端配置 gzip 压缩,并且服务端使用 nginx 开启 gzip,用来减小网络传输的流量大小。

使用 webpack 进行预压缩

使用 compression-webpack-plugin 插件对打包结果进行预压缩,可以移除服务器的压缩时间

js
plugins: [
   // 参考文档配置即可,一般取默认
   new CmpressionWebpackPlugin({
     test: /\.js/, //希望对js进行预压缩
-    minRatio: 0.5 // 小于0.5才会压缩
-  })
-]
+    minRatio: 0.5, // 小于0.5才会压缩
+  }),
+];
 
plugins: [
   // 参考文档配置即可,一般取默认
   new CmpressionWebpackPlugin({
     test: /\.js/, //希望对js进行预压缩
-    minRatio: 0.5 // 小于0.5才会压缩
-  })
-]
-

2.7 按需加载

在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 lodash 这种大型类库同样可以使用这个功能。

按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise,当 Promise 成功以后去执行回调。

2.8 其他

  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css
  • 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径
  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存
  • 提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码

提取第三方库 vendor: 这是也是 webpack 大法的 code splitting,提取一些第三方的库,从而减小 app.js 的大小。 代码层面做好懒加载,网络层面把 CDN、本地缓存用好,前端页面问题基本解决一大半了。剩下主要就是接口层面和“视觉上的快”的优化了,骨架屏先搞起,渲染一个“假页面”占位;接口该合并的合并,该拆分的拆分,如果是可滚动的长页面,就分批次请求

总结:如果有一个工程打包特别大-如何进行优化?

  1. CDN 如果工程中使用了一些知名的第三方库,可以考虑使用 CDN,而不进行打包
  2. 抽离公共模块 如果工程中用到了一些大的公共库,可以考虑将其分割出来单独打包
  3. 异步加载 对于那些不需要在一开始就执行的模块,可以考虑使用动态导入的方式异步加载它们,以尽量减少主包的体积
  4. 压缩、混淆
  5. tree shaking 尽量使用 ESM 语法进行导入导出,充分利用 tree shaking 去除无用代码
  6. gzip 开启 gzip 压缩,进一步减少包体积
  7. 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。

3. 运行性能

运行性能是指,JS代码在浏览器端的运行速度,它主要取决于我们如何书写高性能的代码

永远不要过早的关注于性能,因为你在开发的时候,无法完全预知最终的运行性能,过早的关注性能会极大的降低开发效率 性能优化主要从上面三个维度入手,性能优化没有完美的解决方案,需要具体情况具体分析

4. webpack5 内置优化

  1. webpack scope hoisting

scope hoisting 是 webpack 的内置优化,它是针对模块的优化,在生产环境打包时会自动开启。

在未开启scope hoisting时,webpack 会将每个模块的代码放置在一个独立的函数环境中,这样是为了保证模块的作用域互不干扰。

而 scope hoisting 的作用恰恰相反,是把多个模块的代码合并到一个函数环境中执行。在这一过程中,webpack 会按照顺序正确的合并模块代码,同时对涉及的标识符做适当处理以避免重名。

这样做的好处是减少了函数调用,对运行效率有一定提升,同时也降低了打包体积。

但 scope hoisting 的启用是有前提的,如果遇到某些模块多次被其他模块引用,或者使用了动态导入的模块,或者是非 ESM 的模块,都不会有 scope hoisting。

  1. 清除输出目录

webpack5清除输出目录开箱可用,无须安装clean-webpack-plugin,具体做法如下:

javascript
module.exports = {
+    minRatio: 0.5, // 小于0.5才会压缩
+  }),
+];
+

2.7 按需加载

在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 lodash 这种大型类库同样可以使用这个功能。

按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise,当 Promise 成功以后去执行回调。

2.8 其他

  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤ webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS ⽂件, 利⽤ cssnano (css-loader?minimize)来压缩 css
  • 利⽤ CDN 加速: 在构建过程中,将引⽤的静态资源路径修改为 CDN 上对应的路径。可以利⽤ webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径
  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存
  • 提取公共第三⽅库: SplitChunksPlugin 插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码

提取第三方库 vendor: 这是也是 webpack 大法的 code splitting,提取一些第三方的库,从而减小 app.js 的大小。 代码层面做好懒加载,网络层面把 CDN、本地缓存用好,前端页面问题基本解决一大半了。剩下主要就是接口层面和“视觉上的快”的优化了,骨架屏先搞起,渲染一个“假页面”占位;接口该合并的合并,该拆分的拆分,如果是可滚动的长页面,就分批次请求

总结:如果有一个工程打包特别大-如何进行优化?

  1. CDN 如果工程中使用了一些知名的第三方库,可以考虑使用 CDN,而不进行打包
  2. 抽离公共模块 如果工程中用到了一些大的公共库,可以考虑将其分割出来单独打包
  3. 异步加载 对于那些不需要在一开始就执行的模块,可以考虑使用动态导入的方式异步加载它们,以尽量减少主包的体积
  4. 压缩、混淆
  5. tree shaking 尽量使用 ESM 语法进行导入导出,充分利用 tree shaking 去除无用代码
  6. gzip 开启 gzip 压缩,进一步减少包体积
  7. 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。

3. 运行性能

运行性能是指,JS 代码在浏览器端的运行速度,它主要取决于我们如何书写高性能的代码

永远不要过早的关注于性能,因为你在开发的时候,无法完全预知最终的运行性能,过早的关注性能会极大的降低开发效率 性能优化主要从上面三个维度入手,性能优化没有完美的解决方案,需要具体情况具体分析

4. webpack5 内置优化

  1. webpack scope hoisting

scope hoisting 是 webpack 的内置优化,它是针对模块的优化,在生产环境打包时会自动开启。

在未开启 scope hoisting 时,webpack 会将每个模块的代码放置在一个独立的函数环境中,这样是为了保证模块的作用域互不干扰。

而 scope hoisting 的作用恰恰相反,是把多个模块的代码合并到一个函数环境中执行。在这一过程中,webpack 会按照顺序正确的合并模块代码,同时对涉及的标识符做适当处理以避免重名。

这样做的好处是减少了函数调用,对运行效率有一定提升,同时也降低了打包体积。

但 scope hoisting 的启用是有前提的,如果遇到某些模块多次被其他模块引用,或者使用了动态导入的模块,或者是非 ESM 的模块,都不会有 scope hoisting。

  1. 清除输出目录

webpack5清除输出目录开箱可用,无须安装clean-webpack-plugin,具体做法如下:

javascript
module.exports = {
   output: {
-    clean: true
-  }
-}
+    clean: true,
+  },
+};
 
module.exports = {
   output: {
-    clean: true
-  }
-}
+    clean: true,
+  },
+};
 
  1. top-level-await

webpack5现在允许在模块的顶级代码中直接使用await

javascript
// src/index.js
 const resp = await fetch("http://www.baidu.com");
 const jsonBody = await resp.json();
@@ -667,7 +685,6 @@
     index2: "./src/index2.js",
   },
 };
-
 
// webpack.config.js
 module.exports = {
   mode: "production",
@@ -677,90 +694,85 @@
     index2: "./src/index2.js",
   },
 };
-
-

  1. 打包缓存开箱即用

webpack4中,需要使用cache-loader缓存打包结果以优化之后的打包性能

而在webpack5中,默认就已经开启了打包缓存,无须再安装cache-loader

默认情况下,webpack5是将模块的打包结果缓存到内存中,可以通过cache配置进行更改

javascript
const path = require('path');
+

  1. 打包缓存开箱即用

webpack4中,需要使用cache-loader缓存打包结果以优化之后的打包性能

而在webpack5中,默认就已经开启了打包缓存,无须再安装cache-loader

默认情况下,webpack5是将模块的打包结果缓存到内存中,可以通过cache配置进行更改

javascript
const path = require("path");
 
 module.exports = {
-  mode: 'development',
-  devtool: 'source-map',
-  entry: './src/index.js',
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
   cache: {
-    type: 'filesystem', // 缓存类型,支持:memory、filesystem
-    cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'), // 缓存目录,仅类型为 filesystem 有效
+    type: "filesystem", // 缓存类型,支持:memory、filesystem
+    cacheDirectory: path.resolve(__dirname, "node_modules/.cache/webpack"), // 缓存目录,仅类型为 filesystem 有效
     // 更多配置参考:https://webpack.docschina.org/configuration/other-options/#cache
   },
 };
-
-
const path = require('path');
+
const path = require("path");
 
 module.exports = {
-  mode: 'development',
-  devtool: 'source-map',
-  entry: './src/index.js',
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
   cache: {
-    type: 'filesystem', // 缓存类型,支持:memory、filesystem
-    cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'), // 缓存目录,仅类型为 filesystem 有效
+    type: "filesystem", // 缓存类型,支持:memory、filesystem
+    cacheDirectory: path.resolve(__dirname, "node_modules/.cache/webpack"), // 缓存目录,仅类型为 filesystem 有效
     // 更多配置参考:https://webpack.docschina.org/configuration/other-options/#cache
   },
 };
+

关于cache的更多配置参考:https://webpack.docschina.org/configuration/other-options/#cache

  1. 资源模块

webpack4中,针对资源型文件我们通常使用file-loaderurl-loaderraw-loader进行处理

由于大部分前端项目都会用到资源型文件,因此webpack5原生支持了资源型模块

详见:https://webpack.docschina.org/guides/asset-modules/

javascript
// index.js
+import bigPic from "./assets/big-pic.png"; // 期望得到路径
+import smallPic from "./assets/small-pic.jpg"; // 期望得到base64
+import yueyunpeng from "./assets/yueyunpeng.gif"; // 期望根据文件大小决定是路径还是base64
+import raw from "./assets/raw.txt"; // 期望得到原始文件内容
 
-

关于cache的更多配置参考:https://webpack.docschina.org/configuration/other-options/#cache

  1. 资源模块

webpack4中,针对资源型文件我们通常使用file-loaderurl-loaderraw-loader进行处理

由于大部分前端项目都会用到资源型文件,因此webpack5原生支持了资源型模块

详见:https://webpack.docschina.org/guides/asset-modules/

javascript
// index.js
-import bigPic from './assets/big-pic.png'; // 期望得到路径
-import smallPic from './assets/small-pic.jpg'; // 期望得到base64
-import yueyunpeng from './assets/yueyunpeng.gif'; // 期望根据文件大小决定是路径还是base64
-import raw from './assets/raw.txt'; // 期望得到原始文件内容
-
-console.log('big-pic.png', bigPic);
-console.log('small-pic.jpg', smallPic);
-console.log('yueyunpeng.gif', yueyunpeng);
-console.log('raw.txt', raw);
-
+console.log("big-pic.png", bigPic);
+console.log("small-pic.jpg", smallPic);
+console.log("yueyunpeng.gif", yueyunpeng);
+console.log("raw.txt", raw);
 
// index.js
-import bigPic from './assets/big-pic.png'; // 期望得到路径
-import smallPic from './assets/small-pic.jpg'; // 期望得到base64
-import yueyunpeng from './assets/yueyunpeng.gif'; // 期望根据文件大小决定是路径还是base64
-import raw from './assets/raw.txt'; // 期望得到原始文件内容
-
-console.log('big-pic.png', bigPic);
-console.log('small-pic.jpg', smallPic);
-console.log('yueyunpeng.gif', yueyunpeng);
-console.log('raw.txt', raw);
+import bigPic from "./assets/big-pic.png"; // 期望得到路径
+import smallPic from "./assets/small-pic.jpg"; // 期望得到base64
+import yueyunpeng from "./assets/yueyunpeng.gif"; // 期望根据文件大小决定是路径还是base64
+import raw from "./assets/raw.txt"; // 期望得到原始文件内容
 
-
javascript
// webpack.config.js
-const path = require('path');
-const HtmlWebpackPlugin = require('html-webpack-plugin');
+console.log("big-pic.png", bigPic);
+console.log("small-pic.jpg", smallPic);
+console.log("yueyunpeng.gif", yueyunpeng);
+console.log("raw.txt", raw);
+
javascript
// webpack.config.js
+const path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
 module.exports = {
-  mode: 'development',
-  devtool: 'source-map',
-  entry: './src/index.js',
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
   devServer: {
     port: 8080,
   },
   plugins: [new HtmlWebpackPlugin()],
   output: {
-    filename: 'main.js',
-    path: path.resolve(__dirname, 'dist'),
-    assetModuleFilename: 'assets/[hash:5][ext]', // 在这里自定义资源文件保存的文件名
+    filename: "main.js",
+    path: path.resolve(__dirname, "dist"),
+    assetModuleFilename: "assets/[hash:5][ext]", // 在这里自定义资源文件保存的文件名
   },
   module: {
     rules: [
       {
         test: /\.png/,
-        type: 'asset/resource', // 作用类似于 file-loader
+        type: "asset/resource", // 作用类似于 file-loader
       },
       {
         test: /\.jpg/,
-        type: 'asset/inline', // 作用类似于 url-loader 文件大小不足的场景
+        type: "asset/inline", // 作用类似于 url-loader 文件大小不足的场景
       },
       {
         test: /\.txt/,
-        type: 'asset/source', // 作用类似于 raw-loader
+        type: "asset/source", // 作用类似于 raw-loader
       },
       {
         test: /\.gif/,
-        type: 'asset', // 作用类似于 url-loader。在导出一个 data uri 和发送一个单独的文件之间自动选择
+        type: "asset", // 作用类似于 url-loader。在导出一个 data uri 和发送一个单独的文件之间自动选择
         generator: {
-          filename: 'gif/[hash:5][ext]', // 这里的配置会覆盖 assetModuleFilename
+          filename: "gif/[hash:5][ext]", // 这里的配置会覆盖 assetModuleFilename
         },
         parser: {
           dataUrlCondition: {
@@ -772,42 +784,41 @@
     ],
   },
 };
-
 
// webpack.config.js
-const path = require('path');
-const HtmlWebpackPlugin = require('html-webpack-plugin');
+const path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
 module.exports = {
-  mode: 'development',
-  devtool: 'source-map',
-  entry: './src/index.js',
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
   devServer: {
     port: 8080,
   },
   plugins: [new HtmlWebpackPlugin()],
   output: {
-    filename: 'main.js',
-    path: path.resolve(__dirname, 'dist'),
-    assetModuleFilename: 'assets/[hash:5][ext]', // 在这里自定义资源文件保存的文件名
+    filename: "main.js",
+    path: path.resolve(__dirname, "dist"),
+    assetModuleFilename: "assets/[hash:5][ext]", // 在这里自定义资源文件保存的文件名
   },
   module: {
     rules: [
       {
         test: /\.png/,
-        type: 'asset/resource', // 作用类似于 file-loader
+        type: "asset/resource", // 作用类似于 file-loader
       },
       {
         test: /\.jpg/,
-        type: 'asset/inline', // 作用类似于 url-loader 文件大小不足的场景
+        type: "asset/inline", // 作用类似于 url-loader 文件大小不足的场景
       },
       {
         test: /\.txt/,
-        type: 'asset/source', // 作用类似于 raw-loader
+        type: "asset/source", // 作用类似于 raw-loader
       },
       {
         test: /\.gif/,
-        type: 'asset', // 作用类似于 url-loader。在导出一个 data uri 和发送一个单独的文件之间自动选择
+        type: "asset", // 作用类似于 url-loader。在导出一个 data uri 和发送一个单独的文件之间自动选择
         generator: {
-          filename: 'gif/[hash:5][ext]', // 这里的配置会覆盖 assetModuleFilename
+          filename: "gif/[hash:5][ext]", // 这里的配置会覆盖 assetModuleFilename
         },
         parser: {
           dataUrlCondition: {
@@ -819,30 +830,53 @@
     ],
   },
 };
-
-

字节跳动面试题:说一下项目里有做过哪些 webpack 上的优化

  1. 对传输性能的优化
  • 压缩和混淆 使用 Uglifyjs 或其他类似工具对打包结果进行压缩、混淆,可以有效的减少包体积
  • tree shaking 项目中尽量使用 ESM,可以有效利用 tree shaking 优化,降低包体积
  • 抽离公共模块 将一些公共代码单独打包,这样可以充分利用浏览器缓存,其他代码变动后,不影响公共代码,浏览器可以直接从缓存中找到公共代码。 具体方式有多种,比如 dll、splitChunks
  • 异步加载 对一些可以延迟执行的模块可以使用动态导入的方式异步加载它们,这样在打包结果中,它们会形成单独的包,同时,在页面一开始解析时并不需要加载它们,而是页面解析完成后,执行 JS 的过程中去加载它们。 这样可以显著提高页面的响应速度,在单页应用中尤其有用。
  • CDN 对一些知名的库使用 CDN,不仅可以节省打包时间,还可以显著提升库的加载速度
  • gzip 目前浏览器普遍支持 gzip 格式,因此可以将静态文件均使用 gzip 进行压缩
  • 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。
  1. 对打包过程的优化
  • noParse 很多第三方库本身就是已经打包好的代码,对于这种代码无须再进行解析,可以使用 noParse 配置排除掉这些第三方库
  • externals 对于一些知名的第三方库可以使用 CDN,这部分库可以通过 externals 配置不进行打包
  • 限制 loader 的范围 在使用 loader 的时候,可以通过 exclude 排除掉一些不必要的编译,比如 babel-loader 对于那些已经完成打包的第三方库没有必要再降级一次,可以排除掉
  • 开启 loader 缓存 可以利用cache-loader缓存 loader 的编译结果,避免在源码没有变动时反复编译
  • 开启多线程编译 可以利用thread-loader开启多线程编译,提升编译效率
  • 动态链接库 对于某些需要打包的第三方库,可以使用 dll 的方式单独对其打包,然后 DLLPlugin 将其整合到当前项目中,这样就避免了在开发中频繁去打包这些库
  1. 对开发体验的优化
  • lint 使用 eslint、stylelint 等工具保证团队代码风格一致
  • HMR 使用热替换避免页面刷新导致的状态丢失,提升开发体验

CSS

TIP

CSS 渲染性能优化

  1. 使用 id selector 非常的高效。在使用 id selector 的时候需要注意一点:因为 id 是唯一的,所以不需要既指定 id 又指定 tagName:
css
/* Bad  */
-p#id1 {color:red;}  
+

字节跳动面试题:说一下项目里有做过哪些 webpack 上的优化

  1. 对传输性能的优化
  • 压缩和混淆 使用 Uglifyjs 或其他类似工具对打包结果进行压缩、混淆,可以有效的减少包体积
  • tree shaking 项目中尽量使用 ESM,可以有效利用 tree shaking 优化,降低包体积
  • 抽离公共模块 将一些公共代码单独打包,这样可以充分利用浏览器缓存,其他代码变动后,不影响公共代码,浏览器可以直接从缓存中找到公共代码。 具体方式有多种,比如 dll、splitChunks
  • 异步加载 对一些可以延迟执行的模块可以使用动态导入的方式异步加载它们,这样在打包结果中,它们会形成单独的包,同时,在页面一开始解析时并不需要加载它们,而是页面解析完成后,执行 JS 的过程中去加载它们。 这样可以显著提高页面的响应速度,在单页应用中尤其有用。
  • CDN 对一些知名的库使用 CDN,不仅可以节省打包时间,还可以显著提升库的加载速度
  • gzip 目前浏览器普遍支持 gzip 格式,因此可以将静态文件均使用 gzip 进行压缩
  • 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。
  1. 对打包过程的优化
  • noParse 很多第三方库本身就是已经打包好的代码,对于这种代码无须再进行解析,可以使用 noParse 配置排除掉这些第三方库
  • externals 对于一些知名的第三方库可以使用 CDN,这部分库可以通过 externals 配置不进行打包
  • 限制 loader 的范围 在使用 loader 的时候,可以通过 exclude 排除掉一些不必要的编译,比如 babel-loader 对于那些已经完成打包的第三方库没有必要再降级一次,可以排除掉
  • 开启 loader 缓存 可以利用 cache-loader 缓存 loader 的编译结果,避免在源码没有变动时反复编译
  • 开启多线程编译 可以利用 thread-loader 开启多线程编译,提升编译效率
  • 动态链接库 对于某些需要打包的第三方库,可以使用 dll 的方式单独对其打包,然后 DLLPlugin 将其整合到当前项目中,这样就避免了在开发中频繁去打包这些库
  1. 对开发体验的优化
  • lint 使用 eslint、stylelint 等工具保证团队代码风格一致
  • HMR 使用热替换避免页面刷新导致的状态丢失,提升开发体验

CSS

TIP

CSS 渲染性能优化

  1. 使用 id selector 非常的高效。在使用 id selector 的时候需要注意一点:因为 id 是唯一的,所以不需要既指定 id 又指定 tagName:
css
/* Bad  */
+p#id1 {
+  color: red;
+}
 
 /* Good  */
-#id1 {color:red;}
+#id1 {
+  color: red;
+}
 
/* Bad  */
-p#id1 {color:red;}  
+p#id1 {
+  color: red;
+}
 
 /* Good  */
-#id1 {color:red;}
-
  1. 不要使用 attribute selector,如:p[att1=”val1”]。这样的匹配非常慢。更不要这样写:p[id="id1"]。这样将 id selector 退化成 attribute selector。
css
/* Bad  */
-p[id="jartto"]{color:red;}  
-p[class="blog"]{color:red;}  
+#id1 {
+  color: red;
+}
+
  1. 不要使用 attribute selector,如:p[att1=”val1”]。这样的匹配非常慢。更不要这样写:p[id="id1"]。这样将 id selector 退化成 attribute selector。
css
/* Bad  */
+p[id="jartto"] {
+  color: red;
+}
+p[class="blog"] {
+  color: red;
+}
 /* Good  */
-#jartto{color:red;}  
-.blog{color:red;}
+#jartto {
+  color: red;
+}
+.blog {
+  color: red;
+}
 
/* Bad  */
-p[id="jartto"]{color:red;}  
-p[class="blog"]{color:red;}  
+p[id="jartto"] {
+  color: red;
+}
+p[class="blog"] {
+  color: red;
+}
 /* Good  */
-#jartto{color:red;}  
-.blog{color:red;}
-
  1. 通常将浏览器前缀置于前面,将标准样式属性置于最后,类似:
css
.foo {
+#jartto {
+  color: red;
+}
+.blog {
+  color: red;
+}
+
  1. 通常将浏览器前缀置于前面,将标准样式属性置于最后,类似:
css
.foo {
   -moz-border-radius: 5px;
   border-radius: 5px;
 }
@@ -850,55 +884,55 @@
   -moz-border-radius: 5px;
   border-radius: 5px;
 }
-

这里推荐参阅 CSS 规范-优化方案:http://nec.netease.com/standard/css-optimize.html

  1. 遵守 CSSLint 规则

font-faces         不能使用超过5个web字体

import            禁止使用@import

regex-selectors      禁止使用属性选择器中的正则表达式选择器

universal-selector       禁止使用通用选择器*

unqualified-attributes    禁止使用不规范的属性选择器

zero-units       0后面不要加单位

overqualified-elements    使用相邻选择器时,不要使用不必要的选择器

shorthand          简写样式属性

duplicate-background-images 相同的url在样式表中不超过一次

更多的 CSSLint 规则可以参阅:https://github.com/CSSLint/csslint

  1. 不要使用 @import

使用 @import 引入 CSS 会影响浏览器的并行下载。使用 @import 引用的 CSS 文件只有在引用它的那个 CSS 文件被下载、解析之后,浏览器才会知道还有另外一个 CSS 需要下载,这时才去下载,然后下载后开始解析、构建 Render Tree 等一系列操作。

多个 @import 会导致下载顺序紊乱。在 IE 中,@import 会引发资源文件的下载顺序被打乱,即排列在 @import 后面的 JS 文件先于 @import 下载,并且打乱甚至破坏 @import 自身的并行下载。

  1. 避免过分重排(Reflow)

所谓重排就是浏览器重新计算布局位置与大小。常见的重排元素:

width
-height 
-padding 
-margin 
-display 
-border-width 
-border 
-top 
-position 
-font-size 
-float 
-text-align 
-overflow-y 
-font-weight 
-overflow 
-left 
-font-family 
-line-height 
-vertical-align 
-right 
-clear 
-white-space 
-bottom 
+

这里推荐参阅 CSS 规范-优化方案:http://nec.netease.com/standard/css-optimize.html

  1. 遵守 CSSLint 规则

font-faces         不能使用超过 5 个 web 字体

import            禁止使用@import

regex-selectors      禁止使用属性选择器中的正则表达式选择器

universal-selector       禁止使用通用选择器*

unqualified-attributes    禁止使用不规范的属性选择器

zero-units        0 后面不要加单位

overqualified-elements    使用相邻选择器时,不要使用不必要的选择器

shorthand          简写样式属性

duplicate-background-images 相同的 url 在样式表中不超过一次

更多的 CSSLint 规则可以参阅:https://github.com/CSSLint/csslint

  1. 不要使用 @import

使用 @import 引入 CSS 会影响浏览器的并行下载。使用 @import 引用的 CSS 文件只有在引用它的那个 CSS 文件被下载、解析之后,浏览器才会知道还有另外一个 CSS 需要下载,这时才去下载,然后下载后开始解析、构建 Render Tree 等一系列操作。

多个 @import 会导致下载顺序紊乱。在 IE 中,@import 会引发资源文件的下载顺序被打乱,即排列在 @import 后面的 JS 文件先于 @import 下载,并且打乱甚至破坏 @import 自身的并行下载。

  1. 避免过分重排(Reflow)

所谓重排就是浏览器重新计算布局位置与大小。常见的重排元素:

width
+height
+padding
+margin
+display
+border-width
+border
+top
+position
+font-size
+float
+text-align
+overflow-y
+font-weight
+overflow
+left
+font-family
+line-height
+vertical-align
+right
+clear
+white-space
+bottom
 min-height
 
width
-height 
-padding 
-margin 
-display 
-border-width 
-border 
-top 
-position 
-font-size 
-float 
-text-align 
-overflow-y 
-font-weight 
-overflow 
-left 
-font-family 
-line-height 
-vertical-align 
-right 
-clear 
-white-space 
-bottom 
+height
+padding
+margin
+display
+border-width
+border
+top
+position
+font-size
+float
+text-align
+overflow-y
+font-weight
+overflow
+left
+font-family
+line-height
+vertical-align
+right
+clear
+white-space
+bottom
 min-height
-
  1. 依赖继承。如果某些属性可以继承,那么自然没有必要在写一遍。

  2. 其他:使用 id 选择器非常高效,因为 id 是唯一的;使用渐进增强的方案;值缩写;避免耗性能的属性;背景图优化合并;文件压缩

网络层面

总结

  • 优化打包体积:利用一些工具压缩、混淆最终打包代码,减少包体积
  • 多目标打包:利用一些打包插件,针对不同的浏览器打包出不同的兼容性版本,这样一来,每个版本中的兼容性代码就会大大减少,从而减少包体积
  • 压缩:现代浏览器普遍支持压缩格式,因此服务端的各种文件可以压缩后再响应给客户端,只要解压时间小于优化的传输时间,压缩就是可行的
  • CDN:利用 CDN 可以大幅缩减静态资源的访问时间,特别是对于公共库的访问,可以使用知名的 CDN 资源,这样可以实现跨越站点的缓存
  • 缓存:对于除 HTML 外的所有静态资源均可以开启协商缓存,利用构建工具打包产生的文件 hash 值来置换缓存
  • http2:开启 http2 后,利用其多路复用、头部压缩等特点,充分利用带宽传递大量的文件数据
  • 雪碧图:对于不使用 HTTP2 的场景,可以将多个图片合并为雪碧图,以达到减少文件的目的
  • defer、async:通过 defer 和 async 属性,可以让页面尽早加载 js 文件
  • prefetch、preload:通过 prefetch 属性,可以让页面在空闲时预先下载其他页面可能要用到的资源;通过 preload 属性,可以让页面预先下载本页面可能要用到的资源
  • 多个静态资源域:对于不使用 HTTP2 的场景,将相对独立的静态资源分到多个域中保存,可以让浏览器同时开启多个 TCP 连接,并行下载 (http2之前,浏览器开多个tcp,同一个域下最大数6个,为了多,静态资源分多个域存储,突破6个的限制)

CDN

CDN(Content Delivery Network,内容分发网络)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

典型的CDN系统构成:

  • 分发服务系统: 最基本的工作单元就是Cache设备,cache(边缘cache)负责直接响应最终用户的访问请求,把缓存在本地的内容快速地提供给用户。同时cache还负责与源站点进行内容同步,把更新的内容以及本地没有的内容从源站点获取并保存在本地。Cache设备的数量、规模、总服务能力是衡量一个CDN系统服务能力的最基本的指标。

  • 负载均衡系统: 主要功能是负责对所有发起服务请求的用户进行访问调度,确定提供给用户的最终实际访问地址。两级调度体系分为全局负载均衡(GSLB)和本地负载均衡(SLB)。全局负载均衡主要根据用户就近性原则,通过对每个服务节点进行“最优”判断,确定向用户提供服务的cache的物理位置。本地负载均衡主要负责节点内部的设备负载均衡

  • 运营管理系统: 运营管理系统分为运营管理和网络管理子系统,负责处理业务层面的与外界系统交互所必须的收集、整理、交付工作,包含客户管理、产品管理、计费管理、统计分析等功能。

作用

CDN一般会用来托管Web资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用CDN来加速这些资源的访问。

(1)在性能方面,引入CDN的作用在于:

  • 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快
  • 部分资源请求分配给了CDN,减少了服务器的负载

(2)在安全方面,CDN有助于防御DDoS、MITM等网络攻击:

  • 针对DDoS:通过监控分析异常流量,限制其请求频率
  • 针对MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信

除此之外,CDN作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰)等方面的优势。 CDN还会把文件最小化或者压缩文档的优化

原理

它的基本原理是:架设多台服务器,这些服务器定期从源站拿取资源保存本地,到让不同地域的用户能够通过访问最近的服务器获得资源

CDN和DNS有着密不可分的联系,先来看一下DNS的解析域名过程,在浏览器输入 www.test.com 的解析过程如下:

  1. 检查浏览器缓存
  2. 检查操作系统缓存,常见的如hosts文件
  3. 检查路由器缓存
  4. 如果前几步都没没找到,会向ISP(网络服务提供商)的LDNS服务器查询
  5. 如果LDNS服务器没找到,会向根域名服务器(Root Server)请求解析,分为以下几步:
  • 根服务器返回顶级域名(TLD)服务器如.com,.cn,.org等的地址,该例子中会返回.com的地址
  • 接着向顶级域名服务器发送请求,然后会返回次级域名(SLD)服务器的地址,本例子会返回.test的地址
  • 接着向次级域名服务器发送请求,然后会返回通过域名查询到的目标IP,本例子会返回www.test.com的地址
  • Local DNS Server会缓存结果,并返回给用户,缓存在系统中

CDN的工作原理:

  1. 用户未使用CDN缓存资源的过程:
  2. 浏览器通过DNS对域名进行解析(就是上面的DNS解析过程),依次得到此域名对应的IP地址
  3. 浏览器根据得到的IP地址,向域名的服务主机发送数据请求
  4. 服务器向浏览器返回响应数据

(2)用户使用CDN缓存资源的过程:

  1. 对于点击的数据的URL,经过本地DNS系统的解析,发现该URL对应的是一个CDN专用的DNS服务器,DNS系统就会将域名解析权交给CNAME指向的CDN专用的DNS服务器。
  2. CND专用DNS服务器将CND的全局负载均衡设备IP地址返回给用户
  3. 用户向CDN的全局负载均衡设备发起数据请求
  4. CDN的全局负载均衡设备根据用户的IP地址,以及用户请求的内容URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求
  5. 区域负载均衡设备选择一台合适的缓存服务器来提供服务,将该缓存服务器的IP地址返回给全局负载均衡设备
  6. 全局负载均衡设备把服务器的IP地址返回给用户
  7. 用户向该缓存服务器发起请求,缓存服务器响应用户的请求,将用户所需内容发送至用户终端。

如果缓存服务器没有用户想要的内容,那么缓存服务器就会向它的上一级缓存服务器请求内容,以此类推,直到获取到需要的资源。最后如果还是没有,就会回到自己的服务器去获取资源。

CNAME(意为:别名):在域名解析中,实际上解析出来的指定域名对应的IP地址,或者该域名的一个CNAME,然后再根据这个CNAME来查找对应的IP地址。

使用场景

  • 使用第三方的CDN服务:如果想要开源一些项目,可以使用第三方的CDN服务
  • 使用CDN进行静态资源的缓存:将自己网站的静态资源放在CDN上,比如js、css、图片等。可以将整个项目放在CDN上,完成一键部署。
  • 直播传送:直播本质上是使用流媒体进行传送,CDN也是支持流媒体传送的,所以直播完全可以使用CDN来提高访问速度。CDN在处理流媒体的时候与处理普通静态文件有所不同,普通文件如果在边缘节点没有找到的话,就会去上一层接着寻找,但是流媒体本身数据量就非常大,如果使用回源的方式,必然会带来性能问题,所以流媒体一般采用的都是主动推送的方式来进行。

TODO:cdn加速原理,没有缓存到哪里拿,CDN回源策略

分发的内容

静态内容:即使是静态内容也不是一直保存在cdn,源服务器发送文件给CDN的时候就可以利用HTTP头部的cache-control可以设置文件的缓存形式,cdn就知道哪些内容可以保存no-cache,那些不能no-store,那些保存多久max-age

动态内容:

工作流程:

静态内容:源服务器把静态内容提前备份给cdn(push),世界各地访问的时候就进的cdn服务器会把静态内容提供给用户,不需要每次劳烦源服务器。如果没有提前备份,cdn问源服务器要(pull),然后cdn备份,其他请球的用户可以马上拿到。

动态内容:源服务器很难做到提前预测到每个用户的动态内容提前给到cdn,如果等到用户索取动态内容cdn再向源服务器获取,这样cdn提供不了加速服务。但是有些是可以提供动态服务的:时间,有些cdn会提供可以运行在cdn上的接口,让源服务器用这些cdn接口,而不是源服务器自己的代码,用户就可以直接从cdn获取时间

问:cdn用什么方式来转移流量实现负载均衡?

和DNS域名解析根服务器的做法相似:任播通信:服务器对外都拥有同一个ip地址,如果收到了请求,请求就会由距离用户最近的服务器响应,任播技术把流量转移给没超载的服务器可以缓解。CDN还会用TLS/SSL证书对网站进行保护。 我们可以把项目中的所有静态资源都放到CDN上(收费),也可以利用现成免费的CDN获取公共库的资源

js
 
+
  1. 依赖继承。如果某些属性可以继承,那么自然没有必要在写一遍。

  2. 其他:使用 id 选择器非常高效,因为 id 是唯一的;使用渐进增强的方案;值缩写;避免耗性能的属性;背景图优化合并;文件压缩

网络层面

总结

  • 优化打包体积:利用一些工具压缩、混淆最终打包代码,减少包体积
  • 多目标打包:利用一些打包插件,针对不同的浏览器打包出不同的兼容性版本,这样一来,每个版本中的兼容性代码就会大大减少,从而减少包体积
  • 压缩:现代浏览器普遍支持压缩格式,因此服务端的各种文件可以压缩后再响应给客户端,只要解压时间小于优化的传输时间,压缩就是可行的
  • CDN:利用 CDN 可以大幅缩减静态资源的访问时间,特别是对于公共库的访问,可以使用知名的 CDN 资源,这样可以实现跨越站点的缓存
  • 缓存:对于除 HTML 外的所有静态资源均可以开启协商缓存,利用构建工具打包产生的文件 hash 值来置换缓存
  • http2:开启 http2 后,利用其多路复用、头部压缩等特点,充分利用带宽传递大量的文件数据
  • 雪碧图:对于不使用 HTTP2 的场景,可以将多个图片合并为雪碧图,以达到减少文件的目的
  • defer、async:通过 defer 和 async 属性,可以让页面尽早加载 js 文件
  • prefetch、preload:通过 prefetch 属性,可以让页面在空闲时预先下载其他页面可能要用到的资源;通过 preload 属性,可以让页面预先下载本页面可能要用到的资源
  • 多个静态资源域:对于不使用 HTTP2 的场景,将相对独立的静态资源分到多个域中保存,可以让浏览器同时开启多个 TCP 连接,并行下载 (http2 之前,浏览器开多个 tcp,同一个域下最大数 6 个,为了多,静态资源分多个域存储,突破 6 个的限制)

CDN

CDN(Content Delivery Network,内容分发网络)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

典型的 CDN 系统构成:

  • 分发服务系统: 最基本的工作单元就是 Cache 设备,cache(边缘 cache)负责直接响应最终用户的访问请求,把缓存在本地的内容快速地提供给用户。同时 cache 还负责与源站点进行内容同步,把更新的内容以及本地没有的内容从源站点获取并保存在本地。Cache 设备的数量、规模、总服务能力是衡量一个 CDN 系统服务能力的最基本的指标。

  • 负载均衡系统: 主要功能是负责对所有发起服务请求的用户进行访问调度,确定提供给用户的最终实际访问地址。两级调度体系分为全局负载均衡(GSLB)和本地负载均衡(SLB)。全局负载均衡主要根据用户就近性原则,通过对每个服务节点进行“最优”判断,确定向用户提供服务的 cache 的物理位置。本地负载均衡主要负责节点内部的设备负载均衡

  • 运营管理系统: 运营管理系统分为运营管理和网络管理子系统,负责处理业务层面的与外界系统交互所必须的收集、整理、交付工作,包含客户管理、产品管理、计费管理、统计分析等功能。

作用

CDN 一般会用来托管 Web 资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用 CDN 来加速这些资源的访问。

(1)在性能方面,引入 CDN 的作用在于:

  • 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快
  • 部分资源请求分配给了 CDN,减少了服务器的负载

(2)在安全方面,CDN 有助于防御 DDoS、MITM 等网络攻击:

  • 针对 DDoS:通过监控分析异常流量,限制其请求频率
  • 针对 MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信

除此之外,CDN 作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰)等方面的优势。 CDN 还会把文件最小化或者压缩文档的优化

原理

它的基本原理是:架设多台服务器,这些服务器定期从源站拿取资源保存本地,到让不同地域的用户能够通过访问最近的服务器获得资源

CDN 和 DNS 有着密不可分的联系,先来看一下 DNS 的解析域名过程,在浏览器输入 www.test.com 的解析过程如下:

  1. 检查浏览器缓存
  2. 检查操作系统缓存,常见的如 hosts 文件
  3. 检查路由器缓存
  4. 如果前几步都没没找到,会向 ISP(网络服务提供商)的 LDNS 服务器查询
  5. 如果 LDNS 服务器没找到,会向根域名服务器(Root Server)请求解析,分为以下几步:
  • 根服务器返回顶级域名(TLD)服务器如.com,.cn,.org 等的地址,该例子中会返回.com 的地址
  • 接着向顶级域名服务器发送请求,然后会返回次级域名(SLD)服务器的地址,本例子会返回.test 的地址
  • 接着向次级域名服务器发送请求,然后会返回通过域名查询到的目标 IP,本例子会返回www.test.com的地址
  • Local DNS Server 会缓存结果,并返回给用户,缓存在系统中

CDN 的工作原理:

  1. 用户未使用 CDN 缓存资源的过程:
  2. 浏览器通过 DNS 对域名进行解析(就是上面的 DNS 解析过程),依次得到此域名对应的 IP 地址
  3. 浏览器根据得到的 IP 地址,向域名的服务主机发送数据请求
  4. 服务器向浏览器返回响应数据

(2)用户使用 CDN 缓存资源的过程:

  1. 对于点击的数据的 URL,经过本地 DNS 系统的解析,发现该 URL 对应的是一个 CDN 专用的 DNS 服务器,DNS 系统就会将域名解析权交给 CNAME 指向的 CDN 专用的 DNS 服务器。
  2. CND 专用 DNS 服务器将 CND 的全局负载均衡设备 IP 地址返回给用户
  3. 用户向 CDN 的全局负载均衡设备发起数据请求
  4. CDN 的全局负载均衡设备根据用户的 IP 地址,以及用户请求的内容 URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求
  5. 区域负载均衡设备选择一台合适的缓存服务器来提供服务,将该缓存服务器的 IP 地址返回给全局负载均衡设备
  6. 全局负载均衡设备把服务器的 IP 地址返回给用户
  7. 用户向该缓存服务器发起请求,缓存服务器响应用户的请求,将用户所需内容发送至用户终端。

如果缓存服务器没有用户想要的内容,那么缓存服务器就会向它的上一级缓存服务器请求内容,以此类推,直到获取到需要的资源。最后如果还是没有,就会回到自己的服务器去获取资源。

CNAME(意为:别名):在域名解析中,实际上解析出来的指定域名对应的 IP 地址,或者该域名的一个 CNAME,然后再根据这个 CNAME 来查找对应的 IP 地址。

使用场景

  • 使用第三方的 CDN 服务:如果想要开源一些项目,可以使用第三方的 CDN 服务
  • 使用 CDN 进行静态资源的缓存:将自己网站的静态资源放在 CDN 上,比如 js、css、图片等。可以将整个项目放在 CDN 上,完成一键部署。
  • 直播传送:直播本质上是使用流媒体进行传送,CDN 也是支持流媒体传送的,所以直播完全可以使用 CDN 来提高访问速度。CDN 在处理流媒体的时候与处理普通静态文件有所不同,普通文件如果在边缘节点没有找到的话,就会去上一层接着寻找,但是流媒体本身数据量就非常大,如果使用回源的方式,必然会带来性能问题,所以流媒体一般采用的都是主动推送的方式来进行。

TODO:cdn 加速原理,没有缓存到哪里拿,CDN 回源策略

分发的内容

静态内容:即使是静态内容也不是一直保存在 cdn,源服务器发送文件给 CDN 的时候就可以利用 HTTP 头部的 cache-control 可以设置文件的缓存形式,cdn 就知道哪些内容可以保存 no-cache,那些不能 no-store,那些保存多久 max-age

动态内容:

工作流程:

静态内容:源服务器把静态内容提前备份给 cdn(push),世界各地访问的时候就进的 cdn 服务器会把静态内容提供给用户,不需要每次劳烦源服务器。如果没有提前备份,cdn 问源服务器要(pull),然后 cdn 备份,其他请球的用户可以马上拿到。

动态内容:源服务器很难做到提前预测到每个用户的动态内容提前给到 cdn,如果等到用户索取动态内容 cdn 再向源服务器获取,这样 cdn 提供不了加速服务。但是有些是可以提供动态服务的:时间,有些 cdn 会提供可以运行在 cdn 上的接口,让源服务器用这些 cdn 接口,而不是源服务器自己的代码,用户就可以直接从 cdn 获取时间

问:cdn 用什么方式来转移流量实现负载均衡?

和 DNS 域名解析根服务器的做法相似:任播通信:服务器对外都拥有同一个 ip 地址,如果收到了请求,请求就会由距离用户最近的服务器响应,任播技术把流量转移给没超载的服务器可以缓解。CDN 还会用 TLS/SSL 证书对网站进行保护。 我们可以把项目中的所有静态资源都放到 CDN 上(收费),也可以利用现成免费的 CDN 获取公共库的资源

js

 首先我们需要告诉webpack不要对公共库进行打包
 // vue.config.js
 module.exports = {
@@ -912,7 +946,7 @@
 };
 然后在页面中手动加入cdn链接这里使用bootcn
 对于vuex和vue-router使用这种传统的方式引入的话会自动成为Vue的插件因此需要去掉Vue.use(xxx)
- 
+
 我们可以使用下面的代码来进行兼容
 // store.js
 import Vue from "vue";
@@ -931,7 +965,7 @@
   // 没有使用传统的方式引入VueRouter
   Vue.use(VueRouter);
 }
-
 
+

 首先,我们需要告诉webpack不要对公共库进行打包
 // vue.config.js
 module.exports = {
@@ -945,7 +979,7 @@
 };
 然后,在页面中手动加入cdn链接,这里使用bootcn
 对于vuex和vue-router,使用这种传统的方式引入的话会自动成为Vue的插件,因此需要去掉Vue.use(xxx)
- 
+
 我们可以使用下面的代码来进行兼容
 // store.js
 import Vue from "vue";
@@ -964,27 +998,29 @@
   // 没有使用传统的方式引入VueRouter
   Vue.use(VueRouter);
 }
-

增加带宽

增加带宽可以提高资源的访问速度,从而提高首批的加载速度,我司项目带宽由 2M 升级到 5M,效果明显。

http内置优化

  • Http2
    • 头部压缩:专门的 HPACK 压缩算法
      • 索引表:客户端和服务器共同维护的一张表,表的内容分为 61 位的静态表(保存常用信息,例如:host/content-type)和动态表
      • 霍夫曼编码
  • 链路复用
    • Http1 建立起 Tcp 连接,发送请求之后,服务器在处理请求的等待期间,这个期间又没有数据去发送,称为空挡期。链接断开是在服务器响应回溯之后
      • keep-alive 链接保持一段时间
      • HTTP2 可以利用空档期
      • 不需要再重复建立链接
  • 二进制帧
    • Http1.1 文本字符分割的数据流,解析慢且容易出错
    • 二进制帧:帧长度、帧类型、帧标识 补充:采用 Http2 之后,可以减少资源合并的操作,因为首部压缩已经减少了多请求传输的数据量

数据传输层面

  • 缓存:浏览器缓存
    • 强缓存
      • cache-contorl: max-age=30
      • expires: Wed, 21 Oct 2021 07:28:00 GMT
  • 协商缓存
    • etag
    • last-modified
    • if-modified-since
    • if-none-match
  • 压缩
    • 数据压缩:gzip
    • 代码文件压缩:HTML/CSS/JS 中的注释、空格、长变量等
    • 静态资源:字体图标,去除元数据,缩小尺寸以及分辨率
    • 头与报文
      • http1.1 中减少不必要的头
      • 减少 cookie 数据量

Vue

Vue 开发优化

使用key

对于通过循环生成的列表,应给每个列表项一个稳定且唯一的key,这有利于在列表变动时,尽量少的删除、新增、改动元素

使用冻结的对象

冻结的对象不会被响应化

使用函数式组件

参见函数式组件

使用计算属性

如果模板中某个数据会使用多次,并且该数据是通过计算得到的,使用计算属性以缓存它们

非实时绑定的表单项

当使用v-model绑定一个表单项时,当用户改变表单项的状态时,也会随之改变数据,从而导致vue发生重渲染(rerender),这会带来一些性能的开销。

特别是当用户改变表单项时,页面有一些动画正在进行中,由于JS执行线程和浏览器渲染线程是互斥的,最终会导致动画出现卡顿。

我们可以通过使用lazy或不使用v-model的方式解决该问题,但要注意,这样可能会导致在某一个时间段内数据和表单项的值是不一致的。

保持对象引用稳定

在绝大部分情况下,vue触发rerender的时机是其依赖的数据发生变化

若数据没有发生变化,哪怕给数据重新赋值了,vue也是不会做出任何处理的

下面是vue判断数据没有变化的源码

js
// value 为旧值, newVal 为新值
-if (newVal === value || (newVal !== newVal && value !== value)) {//NaN
-  return
+

增加带宽

增加带宽可以提高资源的访问速度,从而提高首批的加载速度,我司项目带宽由 2M 升级到 5M,效果明显。

http 内置优化

  • Http2
    • 头部压缩:专门的 HPACK 压缩算法
      • 索引表:客户端和服务器共同维护的一张表,表的内容分为 61 位的静态表(保存常用信息,例如:host/content-type)和动态表
      • 霍夫曼编码
  • 链路复用
    • Http1 建立起 Tcp 连接,发送请求之后,服务器在处理请求的等待期间,这个期间又没有数据去发送,称为空挡期。链接断开是在服务器响应回溯之后
      • keep-alive 链接保持一段时间
      • HTTP2 可以利用空档期
      • 不需要再重复建立链接
  • 二进制帧
    • Http1.1 文本字符分割的数据流,解析慢且容易出错
    • 二进制帧:帧长度、帧类型、帧标识 补充:采用 Http2 之后,可以减少资源合并的操作,因为首部压缩已经减少了多请求传输的数据量

数据传输层面

  • 缓存:浏览器缓存
    • 强缓存
      • cache-contorl: max-age=30
      • expires: Wed, 21 Oct 2021 07:28:00 GMT
  • 协商缓存
    • etag
    • last-modified
    • if-modified-since
    • if-none-match
  • 压缩
    • 数据压缩:gzip
    • 代码文件压缩:HTML/CSS/JS 中的注释、空格、长变量等
    • 静态资源:字体图标,去除元数据,缩小尺寸以及分辨率
    • 头与报文
      • http1.1 中减少不必要的头
      • 减少 cookie 数据量

Vue

Vue 开发优化

使用 key

对于通过循环生成的列表,应给每个列表项一个稳定且唯一的 key,这有利于在列表变动时,尽量少的删除、新增、改动元素

使用冻结的对象

冻结的对象不会被响应化

使用函数式组件

参见函数式组件

使用计算属性

如果模板中某个数据会使用多次,并且该数据是通过计算得到的,使用计算属性以缓存它们

非实时绑定的表单项

当使用 v-model 绑定一个表单项时,当用户改变表单项的状态时,也会随之改变数据,从而导致 vue 发生重渲染(rerender),这会带来一些性能的开销。

特别是当用户改变表单项时,页面有一些动画正在进行中,由于 JS 执行线程和浏览器渲染线程是互斥的,最终会导致动画出现卡顿。

我们可以通过使用 lazy 或不使用 v-model 的方式解决该问题,但要注意,这样可能会导致在某一个时间段内数据和表单项的值是不一致的。

保持对象引用稳定

在绝大部分情况下,vue 触发 rerender 的时机是其依赖的数据发生变化

若数据没有发生变化,哪怕给数据重新赋值了,vue 也是不会做出任何处理的

下面是 vue 判断数据没有变化的源码

js
// value 为旧值, newVal 为新值
+if (newVal === value || (newVal !== newVal && value !== value)) {
+  //NaN
+  return;
 }
 
// value 为旧值, newVal 为新值
-if (newVal === value || (newVal !== newVal && value !== value)) {//NaN
-  return
+if (newVal === value || (newVal !== newVal && value !== value)) {
+  //NaN
+  return;
 }
-

因此,如果需要,只要能保证组件的依赖数据不发生变化,组件就不会重新渲染。

对于原始数据类型,保持其值不变即可

对于对象类型,保持其引用不变即可

从另一方面来说,由于可以通过保持属性引用稳定来避免子组件的重渲染,那么我们应该细分组件来尽量避免多余的渲染

使用v-show替代v-if

对于频繁切换显示状态的元素,使用v-show可以保证虚拟dom树的稳定,避免频繁的新增和删除元素,特别是对于那些内部包含大量dom元素的节点,这一点极其重要

关键字:频繁切换显示状态、内部包含大量dom元素

使用延迟装载(defer)

首页白屏时间主要受到两个因素的影响:

  • 打包体积过大 巨型包需要消耗大量的传输时间,导致JS传输完成前页面只有一个<div>,没有可显示的内容 <div id="app">好看的东西<div>

  • 需要立即渲染的内容太多 JS传输完成后,浏览器开始执行JS构造页面。 但可能一开始要渲染的组件太多,不仅JS执行的时间很长,而且执行完后浏览器要渲染的元素过多,从而导致页面白屏

打包体积过大需要自行优化打包体积,本节不予讨论

可以进行分包

本节仅讨论渲染内容太多的问题。

一个可行的办法就是延迟装载组件,让组件按照指定的先后顺序依次一个一个渲染出来

延迟装载是一个思路,本质上就是利用requestAnimationFrame事件分批渲染内容,它的具体实现多种多样

使用keep-alive

keep-alive组件是vue的内置组件,用于缓存内部组件实例。这样做的目的在于,keep-alive内部的组件切回时,不用重新创建组件实例,而直接使用缓存中的实例,一方面能够避免创建组件带来的开销,另一方面可以保留组件的状态(不仅是数据的保留,还要真实dom的保留)。

keep-alive具有include和exclude属性,通过它们可以控制哪些组件进入缓存。另外它还提供了max属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue会移除最久没有使用的组件缓存。

受keep-alive的影响,其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是activated和deactivated,它们分别在组件激活和失活时触发。第一次activated触发是在mounted之后

原理 在具体的实现上,keep-alive在内部维护了一个key数组和一个缓存对象

js
// keep-alive 内部的声明周期函数
+

因此,如果需要,只要能保证组件的依赖数据不发生变化,组件就不会重新渲染。

对于原始数据类型,保持其值不变即可

对于对象类型,保持其引用不变即可

从另一方面来说,由于可以通过保持属性引用稳定来避免子组件的重渲染,那么我们应该细分组件来尽量避免多余的渲染

使用 v-show 替代 v-if

对于频繁切换显示状态的元素,使用 v-show 可以保证虚拟 dom 树的稳定,避免频繁的新增和删除元素,特别是对于那些内部包含大量 dom 元素的节点,这一点极其重要

关键字:频繁切换显示状态、内部包含大量 dom 元素

使用延迟装载(defer)

首页白屏时间主要受到两个因素的影响:

  • 打包体积过大 巨型包需要消耗大量的传输时间,导致 JS 传输完成前页面只有一个<div>,没有可显示的内容 <div id="app">好看的东西<div>

  • 需要立即渲染的内容太多 JS 传输完成后,浏览器开始执行 JS 构造页面。 但可能一开始要渲染的组件太多,不仅 JS 执行的时间很长,而且执行完后浏览器要渲染的元素过多,从而导致页面白屏

打包体积过大需要自行优化打包体积,本节不予讨论

可以进行分包

本节仅讨论渲染内容太多的问题。

一个可行的办法就是延迟装载组件,让组件按照指定的先后顺序依次一个一个渲染出来

延迟装载是一个思路,本质上就是利用 requestAnimationFrame 事件分批渲染内容,它的具体实现多种多样

使用 keep-alive

keep-alive 组件是 vue 的内置组件,用于缓存内部组件实例。这样做的目的在于,keep-alive 内部的组件切回时,不用重新创建组件实例,而直接使用缓存中的实例,一方面能够避免创建组件带来的开销,另一方面可以保留组件的状态(不仅是数据的保留,还要真实 dom 的保留)。

keep-alive 具有 include 和 exclude 属性,通过它们可以控制哪些组件进入缓存。另外它还提供了 max 属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue 会移除最久没有使用的组件缓存。

受 keep-alive 的影响,其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是 activated 和 deactivated,它们分别在组件激活和失活时触发。第一次 activated 触发是在 mounted 之后

原理 在具体的实现上,keep-alive 在内部维护了一个 key 数组和一个缓存对象

js
// keep-alive 内部的声明周期函数
 created () {
   this.cache = Object.create(null)
   this.keys = []
 }
- 
+
 
// keep-alive 内部的声明周期函数
 created () {
   this.cache = Object.create(null)
   this.keys = []
 }
- 
-

key数组记录目前缓存的组件key值,如果组件没有指定key值,则会为其自动生成一个唯一的key值 cache对象以key值为键,vnode为值,用于缓存组件对应的虚拟DOM

在keep-alive的渲染函数中,其基本逻辑是判断当前渲染的vnode是否有对应的缓存,如果有,从缓存中读取到对应的组件实例;如果没有则将其缓存。 当缓存数量超过max数值时,keep-alive会移除掉key数组的第一个元素

js
render(){
+
+

key 数组记录目前缓存的组件 key 值,如果组件没有指定 key 值,则会为其自动生成一个唯一的 key 值 cache 对象以 key 值为键,vnode 为值,用于缓存组件对应的虚拟 DOM

在 keep-alive 的渲染函数中,其基本逻辑是判断当前渲染的 vnode 是否有对应的缓存,如果有,从缓存中读取到对应的组件实例;如果没有则将其缓存。 当缓存数量超过 max 数值时,keep-alive 会移除掉 key 数组的第一个元素

js
render(){
   const slot = this.$slots.default; // 获取默认插槽
   const vnode = getFirstComponentChild(slot); // 得到插槽中的第一个组件的vnode
   const name = getComponentName(vnode.componentOptions); //获取组件名字
@@ -996,7 +1032,7 @@
     vnode.componentInstance = cache[key].componentInstance
     remove(keys, key); // 删除key
     // 将key加入到数组末尾,这样是为了保证最近使用的组件在数组中靠后,反之靠前
-    keys.push(key); 
+    keys.push(key);
   } else {
     // 无缓存,进行缓存
     cache[key] = vnode
@@ -1020,7 +1056,7 @@
     vnode.componentInstance = cache[key].componentInstance
     remove(keys, key); // 删除key
     // 将key加入到数组末尾,这样是为了保证最近使用的组件在数组中靠后,反之靠前
-    keys.push(key); 
+    keys.push(key);
   } else {
     // 无缓存,进行缓存
     cache[key] = vnode
@@ -1032,23 +1068,23 @@
   }
   return vnode;
 }
-

长列表优化

vue-virtual-scroller

首先这个库在使用上是很方便的,就是它提供了一个标签,相当于是对div标签的一个修改,可以实现列表的渲染等等功能

异步组件

在代码层面,vue组件本质上是一个配置对象

js
var comp = {
+

长列表优化

vue-virtual-scroller

首先这个库在使用上是很方便的,就是它提供了一个标签,相当于是对 div 标签的一个修改,可以实现列表的渲染等等功能

异步组件

在代码层面,vue 组件本质上是一个配置对象

js
var comp = {
   props: xxx,
   data: xxx,
   computed: xxx,
-  methods: xxx
-}
+  methods: xxx,
+};
 
var comp = {
   props: xxx,
   data: xxx,
   computed: xxx,
-  methods: xxx
-}
-

但有的时候,要得到某个组件配置对象需要一个异步的加载过程,比如:

  • 需要使用ajax获得某个数据之后才能加载该组件
  • 为了合理的分包,组件配置对象需要通过import(xxx)动态加载

如果一个组件需要通过异步的方式得到组件配置对象,该组件可以把它做成一个异步组件

js
/**
+  methods: xxx,
+};
+

但有的时候,要得到某个组件配置对象需要一个异步的加载过程,比如:

  • 需要使用 ajax 获得某个数据之后才能加载该组件
  • 为了合理的分包,组件配置对象需要通过 import(xxx)动态加载

如果一个组件需要通过异步的方式得到组件配置对象,该组件可以把它做成一个异步组件

js
/**
  * 异步组件本质上是一个函数
  * 该函数调用后返回一个Promise,Promise成功的结果是一个组件配置对象
  */
-const AsyncComponent = () => import("./MyComp")
+const AsyncComponent = () => import("./MyComp");
 
 var App = {
   components: {
@@ -1056,14 +1092,14 @@
      * 你可以把该函数当做一个组件使用(异步组件)
      * Vue会调用该函数,并等待Promise完成,完成之前该组件位置什么也不渲染
      */
-    AsyncComponent 
-  }
-}
+    AsyncComponent,
+  },
+};
 
/**
  * 异步组件本质上是一个函数
  * 该函数调用后返回一个Promise,Promise成功的结果是一个组件配置对象
  */
-const AsyncComponent = () => import("./MyComp")
+const AsyncComponent = () => import("./MyComp");
 
 var App = {
   components: {
@@ -1071,26 +1107,32 @@
      * 你可以把该函数当做一个组件使用(异步组件)
      * Vue会调用该函数,并等待Promise完成,完成之前该组件位置什么也不渲染
      */
-    AsyncComponent 
-  }
-}
-

异步组件的函数不仅可以返回一个Promise,还支持返回一个对象

应用:异步组件通常应用在路由懒加载中,以达到更好的分包;为了提高用户体验,可以在组件配置对象加载完成之前给用户显示一些提示信息

js
var routes = [
-  { path: "/", component: async () => {
-    console.log("组件开始加载"); 
-    const HomeComp = await import("./Views/Home.vue");
-    console.log("组件加载完毕");
-    return HomeComp;
-  } }
-]
+    AsyncComponent,
+  },
+};
+

异步组件的函数不仅可以返回一个 Promise,还支持返回一个对象

应用:异步组件通常应用在路由懒加载中,以达到更好的分包;为了提高用户体验,可以在组件配置对象加载完成之前给用户显示一些提示信息

js
var routes = [
+  {
+    path: "/",
+    component: async () => {
+      console.log("组件开始加载");
+      const HomeComp = await import("./Views/Home.vue");
+      console.log("组件加载完毕");
+      return HomeComp;
+    },
+  },
+];
 
var routes = [
-  { path: "/", component: async () => {
-    console.log("组件开始加载"); 
-    const HomeComp = await import("./Views/Home.vue");
-    console.log("组件加载完毕");
-    return HomeComp;
-  } }
-]
-

Vue3 内置优化

静态提升

下面的静态节点会被提升

  • 元素节点
  • 没有绑定动态内容
js

+  {
+    path: "/",
+    component: async () => {
+      console.log("组件开始加载");
+      const HomeComp = await import("./Views/Home.vue");
+      console.log("组件加载完毕");
+      return HomeComp;
+    },
+  },
+];
+

Vue3 内置优化

静态提升

下面的静态节点会被提升

  • 元素节点
  • 没有绑定动态内容
js

 // vue2 的静态节点
 render(){
   createVNode("h1", null, "Hello World")
@@ -1117,7 +1159,7 @@
 

静态属性会被提升

js
<div class="user">
   {{user.name}}
 </div>
- 
+
 const hoisted = { class: "user" }
 
 function render(){
@@ -1127,7 +1169,7 @@
 
<div class="user">
   {{user.name}}
 </div>
- 
+
 const hoisted = { class: "user" }
 
 function render(){
@@ -1166,10 +1208,14 @@
     <span>{{ user.name }}</span>
   </div>
 </div>
-

当编译器遇到大量连续(少量则不会,目前是至少20个连续节点)的静态内容,会直接将其编译为一个普通字符串节点

js
const _hoisted_2 = _createStaticVNode("<div class=\"logo\"><h1>logo</h1></div><ul class=\"nav\"><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li></ul>")
-
const _hoisted_2 = _createStaticVNode("<div class=\"logo\"><h1>logo</h1></div><ul class=\"nav\"><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li></ul>")
-

对ssr的作用非常明显

缓存事件处理函数

js
<button @click="count++">plus</button>
- 
+

当编译器遇到大量连续(少量则不会,目前是至少 20 个连续节点)的静态内容,会直接将其编译为一个普通字符串节点

js
const _hoisted_2 = _createStaticVNode(
+  '<div class="logo"><h1>logo</h1></div><ul class="nav"><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li></ul>'
+);
+
const _hoisted_2 = _createStaticVNode(
+  '<div class="logo"><h1>logo</h1></div><ul class="nav"><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li></ul>'
+);
+

对 ssr 的作用非常明显

缓存事件处理函数

js
<button @click="count++">plus</button>
+
 // vue2
 render(ctx){
   return createVNode("button", {
@@ -1185,9 +1231,9 @@
     onClick: cache[0] || (cache[0] = ($event) => (ctx.count++))//有的话,缓存;没有,count++
   })
 }
- 
+
 
<button @click="count++">plus</button>
- 
+
 // vue2
 render(ctx){
   return createVNode("button", {
@@ -1203,8 +1249,8 @@
     onClick: cache[0] || (cache[0] = ($event) => (ctx.count++))//有的话,缓存;没有,count++
   })
 }
- 
-

Block Tree

vue2在对比新旧树的时候,并不知道哪些节点是静态的,哪些是动态的,因此只能一层一层比较,这就浪费了大部分时间在比对静态节点上

Block节点记录了那些是动态的节点,对比的时候只对比动态节点

html
<form>
+
+

Block Tree

vue2 在对比新旧树的时候,并不知道哪些节点是静态的,哪些是动态的,因此只能一层一层比较,这就浪费了大部分时间在比对静态节点上

Block 节点记录了那些是动态的节点,对比的时候只对比动态节点

html
<form>
   <div>
     <label>账号:</label>
     <input v-model="user.loginId" />
@@ -1214,7 +1260,6 @@
     <input v-model="user.loginPwd" />
   </div>
 </form>
-
 
<form>
   <div>
     <label>账号:</label>
@@ -1225,14 +1270,13 @@
     <input v-model="user.loginPwd" />
   </div>
 </form>
-
-

编译器 会把所有的动态节点标记,存到到根节点的数组中 ,到时候对比的时候只对比block动态节点。如果树不稳定,会有其他方案。

PatchFlag

依托于vue3强大的编译器。vue2在对比每一个节点时,并不知道这个节点哪些相关信息会发生变化,因此只能将所有信息依次比对

js
<div class="user" data-id="1" title="user name">
+

编译器 会把所有的动态节点标记,存到到根节点的数组中 ,到时候对比的时候只对比 block 动态节点。如果树不稳定,会有其他方案。

PatchFlag

依托于 vue3 强大的编译器。vue2 在对比每一个节点时,并不知道这个节点哪些相关信息会发生变化,因此只能将所有信息依次比对

js
<div class="user" data-id="1" title="user name">
   {{user.name}}
 </div>
 
<div class="user" data-id="1" title="user name">
   {{user.name}}
 </div>
-

标识:

  • 标识1:代表元素内容是动态的
  • 标识2:Class
  • 标识3:class+text

启用现代模式

为了兼容各种浏览器,vue-cli在内部使用了@babel/present-env对代码进行降级,你可以通过.browserlistrc配置来设置需要兼容的目标浏览器

这是一种比较偷懒的办法,因为对于那些使用现代浏览器的用户,它们也被迫使用了降级之后的代码,而降低的代码中包含了大量的polyfill,从而提升了包的体积

因此,我们希望提供两种打包结果:

  1. 降级后的包(大),提供给旧浏览器用户使用
  2. 未降级的包(小),提供给现代浏览器用户使用

除了应用webpack进行多次打包外,还可以利用vue-cli给我们提供的命令:

shell
vue-cli-service build --modern
+

标识:

  • 标识 1:代表元素内容是动态的
  • 标识 2:Class
  • 标识 3:class+text

启用现代模式

为了兼容各种浏览器,vue-cli 在内部使用了@babel/present-env 对代码进行降级,你可以通过.browserlistrc 配置来设置需要兼容的目标浏览器

这是一种比较偷懒的办法,因为对于那些使用现代浏览器的用户,它们也被迫使用了降级之后的代码,而降低的代码中包含了大量的 polyfill,从而提升了包的体积

因此,我们希望提供两种打包结果:

  1. 降级后的包(大),提供给旧浏览器用户使用
  2. 未降级的包(小),提供给现代浏览器用户使用

除了应用 webpack 进行多次打包外,还可以利用 vue-cli 给我们提供的命令:

shell
vue-cli-service build --modern
 
vue-cli-service build --modern
 

问题梳理

如何实现 vue 项目中的性能优化?

编码阶段

  • 尽量减少 data 中的数据,data 中的数据都会增加 gettersetter,会收集对应的 watcher
  • v-ifv-for 不能连用
  • 如果需要使用 v-for 给每项元素绑定事件时使用事件代理
  • SPA 页面采用 keep-alive 缓存组件
  • 在更多的情况下,使用 v-if 替代 v-show
  • key 保证唯一
  • 使用路由懒加载、异步组件
  • 防抖、节流
  • 第三方模块按需导入
  • 长列表滚动到可视区域动态加载
  • 图片懒加载

SEO 优化

  • 预渲染
  • 服务端渲染 SSR

打包优化

  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 使用 cdn 加载第三方模块
  • 多线程打包 happypack
  • splitChunks 抽离公共文件
  • sourceMap 优化

用户体验

  • 骨架屏
  • PWA

还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启 gzip 压缩等。

vue 中的 spa 应用如何优化首屏加载速度?

优化首屏加载可以从这几个方面开始:

  • 请求优化:CDN 将第三方的类库放到 CDN 上,能够大幅度减少生产环境中的项目体积,另外 CDN 能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。
  • 缓存:将长时间不会改变的第三方类库或者静态资源设置为强缓存,将 max-age 设置为一个非常长的时间,再将访问路径加上哈希达到哈希值变了以后保证获取到最新资源,好的缓存策略有助于减轻服务器的压力,并且显著的提升用户的体验
  • gzip:开启 gzip 压缩,通常开启 gzip 压缩能够有效的缩小传输资源的大小。
  • http2:如果系统首屏同一时间需要加载的静态资源非常多,但是浏览器对同域名的 tcp 连接数量是有限制的(chrome 为 6 个)超过规定数量的 tcp 连接,则必须要等到之前的请求收到响应后才能继续发送,而 http2 则可以在多个 tcp 连接中并发多个请求没有限制,在一些网络较差的环境开启 http2 性能提升尤为明显。
  • 懒加载:当 url 匹配到相应的路径时,通过 import 动态加载页面组件,这样首屏的代码量会大幅减少,webpack 会把动态加载的页面组件分离成单独的一个 chunk.js 文件
  • 预渲染:由于浏览器在渲染出页面之前,需要先加载和解析相应的 html、css 和 js 文件,为此会有一段白屏的时间,可以添加 loading,或者骨架屏幕尽可能的减少白屏对用户的影响体积优化
  • 合理使用第三方库:对于一些第三方 ui 框架、类库,尽量使用按需加载,减少打包体积
  • 使用可视化工具分析打包后的模块体积:webpack-bundle- analyzer 这个插件在每次打包后能够更加直观的分析打包后模块的体积,再对其中比较大的模块进行优化
  • 提高代码使用率:利用代码分割,将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程
  • 封装:构建良好的项目架构,按照项目需求就行全局组件,插件,过滤器,指令,utils 等做一 些公共封装,可以有效减少我们的代码量,而且更容易维护资源优化
  • 图片懒加载:使用图片懒加载可以优化同一时间减少 http 请求开销,避免显示图片导致的画面抖动,提高用户体验
  • 使用 svg 图标:相对于用一张图片来表示图标,svg 拥有更好的图片质量,体积更小,并且不需要开启额外的 http 请求
  • 压缩图片:可以使用 image-webpack-loader,在用户肉眼分辨不清的情况下一定程度上压缩图片

React

总结

shouldComponentUpdate 提供了两个参数 nextProps 和 nextState,表示下一次 props 和一次 state 的值,当函数返回 false 时候,render()方法不执行,组件也就不会渲染,返回 true 时,组件照常重渲染。此方法就是拿当前 props 中值和下一次 props 中的值进行对比,数据相等时,返回 false,反之返回 true。

需要注意,在进行新旧对比的时候,是**浅对比,**也就是说如果比较的数据时引用数据类型,只要数据的引用的地址没变,即使内容变了,也会被判定为 true。

面对这个问题,可以使用如下方法进行解决: (1)使用 setState 改变数据之前,先采用 ES6 中 assgin 进行拷贝,但是 assgin 只深拷贝的数据的第一层,所以说不是最完美的解决办法:

javascript
const o2 = Object.assign({}, this.state.obj);
 o2.student.count = "00000";
@@ -1254,72 +1298,72 @@
 this.setState({
   obj: o2,
 });
-

React 如何判断什么时候重新渲染组件?

组件状态的改变可以因为props的改变,或者直接通过setState方法改变。组件获得新的状态,然后 React 决定是否应该重新渲染组件。只要组件的 state 发生变化,React 就会对组件进行重新渲染。这是因为 React 中的shouldComponentUpdate方法默认返回true,这就是导致每次更新都重新渲染的原因。

当 React 将要渲染组件时会执行shouldComponentUpdate方法来看它是否返回true(组件应该更新,也就是重新渲染)。所以需要重写shouldComponentUpdate方法让它根据情况返回true或者false来告诉 React 什么时候重新渲染什么时候跳过重新渲染。

避免不必要的 render

React 基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。大多数情况下,React 对 DOM 的渲染效率足以业务日常。但在个别复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,其很重要的一个方向,就是避免不必要的渲染(Render)。这里提下优化的点:

  • shouldComponentUpdate 和 PureComponent

在 React 类组件中,可以利用 shouldComponentUpdate 或者 PureComponent 来减少因父组件更新而触发子组件的 render,从而达到目的。shouldComponentUpdate 来决定是否组件是否重新渲染,如果不希望组件重新渲染,返回 false 即可。

  • 利用高阶组件

在函数组件中,并没有 shouldComponentUpdate 这个生命周期,可以利用高阶组件,封装一个类似 PureComponet 的功能

  • 使用 React.memo

React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo 只能用于函数组件。

https://juejin.cn/post/6935584878071119885

高性能JavaScript

开发注意

遵循严格模式:"use strict"

将 JavaScript 本放在页面底部,加快渲染页面

将 JavaScript 脚本将脚本成组打包,减少请求

使用非阻塞方式下载 JavaScript 脚本

尽量使用局部变量来保存全局变量

尽量减少使用闭包

使用 window 对象属性方法时,省略 window

尽量减少对象成员嵌套

缓存 DOM 节点的访问

通过避免使用 eval() 和 Function() 构造器

给 setTimeout() 和 setInterval() 传递函数而不是字符串作为参数

尽量使用直接量创建对象和数组

最小化重绘 (repaint) 和回流 (reflow)

懒加载

懒加载的概念

懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。 如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。

懒加载的特点

  • 减少无用资源的加载:使用懒加载明显减少了服务器的压力和流量,同时也减小了浏览器的负担。
  • 提升用户体验: 如果同时加载较多图片,可能需要等待的时间较长,这样影响了用户体验,而使用懒加载就能大大的提高用户体验。
  • 防止加载过多图片而影响其他资源文件的加载 :会影响网站应用的正常使用。

懒加载的实现原理

图片的加载是由src引起的,当对src赋值时,浏览器就会请求图片资源。根据这个原理,我们使用HTML5 的data-xxx属性来储存图片的路径,在需要加载图片的时候,将data-xxx中图片的路径赋值给src,这样就实现了图片的按需加载,即懒加载。

注意:data-xxx 中的xxx可以自定义,这里我们使用data-src来定义。 懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可。

使用原生JavaScript实现懒加载

  1. IntersectionObserver api
  2. window.innerHeight 是浏览器可视区的高度
  3. document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离
  4. imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离)
  5. 图片加载条件:img.offsetTop < window.innerHeight + document.body.scrollTop;
html
<div class="container">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
+

React 如何判断什么时候重新渲染组件?

组件状态的改变可以因为props的改变,或者直接通过setState方法改变。组件获得新的状态,然后 React 决定是否应该重新渲染组件。只要组件的 state 发生变化,React 就会对组件进行重新渲染。这是因为 React 中的shouldComponentUpdate方法默认返回true,这就是导致每次更新都重新渲染的原因。

当 React 将要渲染组件时会执行shouldComponentUpdate方法来看它是否返回true(组件应该更新,也就是重新渲染)。所以需要重写shouldComponentUpdate方法让它根据情况返回true或者false来告诉 React 什么时候重新渲染什么时候跳过重新渲染。

避免不必要的 render

React 基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。大多数情况下,React 对 DOM 的渲染效率足以业务日常。但在个别复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,其很重要的一个方向,就是避免不必要的渲染(Render)。这里提下优化的点:

  • shouldComponentUpdate 和 PureComponent

在 React 类组件中,可以利用 shouldComponentUpdate 或者 PureComponent 来减少因父组件更新而触发子组件的 render,从而达到目的。shouldComponentUpdate 来决定是否组件是否重新渲染,如果不希望组件重新渲染,返回 false 即可。

  • 利用高阶组件

在函数组件中,并没有 shouldComponentUpdate 这个生命周期,可以利用高阶组件,封装一个类似 PureComponet 的功能

  • 使用 React.memo

React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo 只能用于函数组件。

https://juejin.cn/post/6935584878071119885

高性能 JavaScript

开发注意

遵循严格模式:"use strict"

将 JavaScript 本放在页面底部,加快渲染页面

将 JavaScript 脚本将脚本成组打包,减少请求

使用非阻塞方式下载 JavaScript 脚本

尽量使用局部变量来保存全局变量

尽量减少使用闭包

使用 window 对象属性方法时,省略 window

尽量减少对象成员嵌套

缓存 DOM 节点的访问

通过避免使用 eval() 和 Function() 构造器

给 setTimeout() 和 setInterval() 传递函数而不是字符串作为参数

尽量使用直接量创建对象和数组

最小化重绘 (repaint) 和回流 (reflow)

懒加载

懒加载的概念

懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。 如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。

懒加载的特点

  • 减少无用资源的加载:使用懒加载明显减少了服务器的压力和流量,同时也减小了浏览器的负担。
  • 提升用户体验: 如果同时加载较多图片,可能需要等待的时间较长,这样影响了用户体验,而使用懒加载就能大大的提高用户体验。
  • 防止加载过多图片而影响其他资源文件的加载 :会影响网站应用的正常使用。

懒加载的实现原理

图片的加载是由 src 引起的,当对 src 赋值时,浏览器就会请求图片资源。根据这个原理,我们使用 HTML5 的 data-xxx 属性来储存图片的路径,在需要加载图片的时候,将 data-xxx 中图片的路径赋值给 src,这样就实现了图片的按需加载,即懒加载。

注意:data-xxx 中的 xxx 可以自定义,这里我们使用 data-src 来定义。 懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可。

使用原生 JavaScript 实现懒加载

  1. IntersectionObserver api
  2. window.innerHeight 是浏览器可视区的高度
  3. document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离
  4. imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离)
  5. 图片加载条件:img.offsetTop < window.innerHeight + document.body.scrollTop;
html
<div class="container">
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
 </div>
 <script>
-  var imgs = document.querySelectorAll('img');
-  function lozyLoad(){
-    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
-    var winHeight= window.innerHeight;
-    for(var i=0;i < imgs.length;i++){
-      if(imgs[i].offsetTop < scrollTop + winHeight ){
-        imgs[i].src = imgs[i].getAttribute('data-src');
+  var imgs = document.querySelectorAll("img");
+  function lozyLoad() {
+    var scrollTop =
+      document.body.scrollTop || document.documentElement.scrollTop;
+    var winHeight = window.innerHeight;
+    for (var i = 0; i < imgs.length; i++) {
+      if (imgs[i].offsetTop < scrollTop + winHeight) {
+        imgs[i].src = imgs[i].getAttribute("data-src");
       }
     }
   }
   window.onscroll = lozyLoad();
 </script>
 
<div class="container">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
-  <img src="loading.gif"  data-src="pic.png">
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
 </div>
 <script>
-  var imgs = document.querySelectorAll('img');
-  function lozyLoad(){
-    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
-    var winHeight= window.innerHeight;
-    for(var i=0;i < imgs.length;i++){
-      if(imgs[i].offsetTop < scrollTop + winHeight ){
-        imgs[i].src = imgs[i].getAttribute('data-src');
+  var imgs = document.querySelectorAll("img");
+  function lozyLoad() {
+    var scrollTop =
+      document.body.scrollTop || document.documentElement.scrollTop;
+    var winHeight = window.innerHeight;
+    for (var i = 0; i < imgs.length; i++) {
+      if (imgs[i].offsetTop < scrollTop + winHeight) {
+        imgs[i].src = imgs[i].getAttribute("data-src");
       }
     }
   }
   window.onscroll = lozyLoad();
 </script>
-

懒加载与预加载的区别

这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力

  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。
  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。 通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。

参考:https://juejin.cn/post/6844903455048335368#heading-5

回流与重绘

回流(重排)

当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流。 下面这些操作会导致回流:

  • 页面的首次渲染
  • 浏览器的窗口大小发生变化
  • 元素的内容发生变化
  • 元素的尺寸或者位置发生变化
  • 元素的字体大小发生变化
  • 激活CSS伪类
  • 查询某些属性或者调用某些方法
  • 添加或者删除可见的DOM元素、
  • 操作class属性
  • 设置style属性:.style....style...----->.class{}

在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的DOM元素重新排列,它的影响范围有两种:

  • 全局范围:从根节点开始,对整个渲染树进行重新布局
  • 局部范围:对渲染树的某部分或者一个渲染对象进行重新布局

重绘

当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。

下面这些操作会导致回流:

  • color、background 相关属性:background-color、background-image 等
  • outline 相关属性:outline-color、outline-width 、text-decoration
  • border-radius、visibility、box-shadow

注意: 当触发回流时,一定会触发重绘,但是重绘不一定会引发回流。

如何避免回流与重绘?

减少回流与重绘的措施:

  • 操作DOM时,尽量在低层级的DOM节点进行操作
  • 不要使用table布局, 一个小的改动可能会使整个table进行重新布局
  • 使用CSS的表达式
  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。
  • 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  • 避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中
  • 将元素先设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制。

浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列

浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。

上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。

如何优化动画?

对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作DOM,就就会导致页面的性能问题,我们可以将动画的position属性设置为absolute或者fixed,将动画脱离文档流,这样他的回流就不会影响到页面了。

CPU中央处理器,擅长逻辑运算

GPU显卡,擅长图片绘制,高精度的浮点数运算。{家用,专业},尽量少复杂动画,即少了GPU,烧性能。

在gpu层面上操作:改变opacity或者 transform:translate3d()/translatez();

最好添加translatez(0); 小hack告诉浏览器告诉浏览器另起一个层

css 1、使用transform 替代top 2、使用visibility 替换display:none ,因为前者只会引起重绘,后者会引发回流(改变了布局 3、避免使用table布局,可能很小的一个小改动会造成整个table的重新布局。 4、尽可能在DOM树的最末端改变class,回流是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点。 5、避免设置多层内联样式,CSS选择符从右往左匹配查找,避免节点层级过多。 CSS3硬件加速(GPU加速),使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。 js 1、避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。 2、避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。 3、避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。 4、对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

新方法:will-change:transform;专门处理GPU加速问题

应用:hover上去后才告诉浏览器要开启新层,点击才触发,总之提前一刻告诉就行

css
div{
+

懒加载与预加载的区别

这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力

  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。
  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。 通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。

参考:https://juejin.cn/post/6844903455048335368#heading-5

回流与重绘

回流(重排)

当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流。 下面这些操作会导致回流:

  • 页面的首次渲染
  • 浏览器的窗口大小发生变化
  • 元素的内容发生变化
  • 元素的尺寸或者位置发生变化
  • 元素的字体大小发生变化
  • 激活 CSS 伪类
  • 查询某些属性或者调用某些方法
  • 添加或者删除可见的 DOM 元素、
  • 操作 class 属性
  • 设置 style 属性:.style....style...----->.class{}

在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的 DOM 元素重新排列,它的影响范围有两种:

  • 全局范围:从根节点开始,对整个渲染树进行重新布局
  • 局部范围:对渲染树的某部分或者一个渲染对象进行重新布局

重绘

当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。

下面这些操作会导致回流:

  • color、background 相关属性:background-color、background-image 等
  • outline 相关属性:outline-color、outline-width 、text-decoration
  • border-radius、visibility、box-shadow

注意: 当触发回流时,一定会触发重绘,但是重绘不一定会引发回流。

如何避免回流与重绘?

减少回流与重绘的措施:

  • 操作 DOM 时,尽量在低层级的 DOM 节点进行操作
  • 不要使用 table 布局, 一个小的改动可能会使整个 table 进行重新布局
  • 使用 CSS 的表达式
  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。
  • 使用 absolute 或者 fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  • 避免频繁操作 DOM,可以创建一个文档片段 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中
  • 将元素先设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
  • 将 DOM 的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制。

浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列

浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。

上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。

如何优化动画?

对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作 DOM,就就会导致页面的性能问题,我们可以将动画的 position 属性设置为 absolute 或者 fixed,将动画脱离文档流,这样他的回流就不会影响到页面了。

CPU 中央处理器,擅长逻辑运算

GPU 显卡,擅长图片绘制,高精度的浮点数运算。{家用,专业},尽量少复杂动画,即少了 GPU,烧性能。

在 gpu 层面上操作:改变 opacity 或者 transform:translate3d()/translatez();

最好添加 translatez(0); 小 hack 告诉浏览器告诉浏览器另起一个层

css 1、使用 transform 替代 top 2、使用 visibility 替换 display:none ,因为前者只会引起重绘,后者会引发回流(改变了布局 3、避免使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。 4、尽可能在 DOM 树的最末端改变 class,回流是不可避免的,但可以减少其影响。尽可能在 DOM 树的最末端改变 class,可以限制了回流的范围,使其影响尽可能少的节点。 5、避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。 CSS3 硬件加速(GPU 加速),使用 css3 硬件加速,可以让 transform、opacity、filters 这些动画不会引起回流重绘。但是对于动画的其它属性,比如 background-color 这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。 js 1、避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。 2、避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。 3、避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。 4、对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

新方法:will-change:transform;专门处理 GPU 加速问题

应用:hover 上去后才告诉浏览器要开启新层,点击才触发,总之提前一刻告诉就行

css
div {
   width: 100px;
   height: 100px;
 }
-div.hover{
+div.hover {
   will-change: transform;
 }
-div.active{
+div.active {
   transform: scale(2, 3);
 }
-
-
div{
+
div {
   width: 100px;
   height: 100px;
 }
-div.hover{
+div.hover {
   will-change: transform;
 }
-div.active{
+div.active {
   transform: scale(2, 3);
 }
-
-

浏览器刷新页面的频率1s 60s

每16.7mm刷新一次

gpu 可以再一帧里渲染好页面,那么当你改动页面的元素或者实现动画的时候,将会非常流畅

documentFragment 是什么?用它跟直接操作 DOM 的区别是什么?

MDN中对documentFragment的解释:

DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。

当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的DOM操作时,我们就可以将DOM元素插入DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作DOM相比,将DocumentFragment 节点插入DOM树时,不会触发页面的重绘,这样就大大提高了页面的性能。

防抖函数

  • 按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次
  • 服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤lodash.debounce 节流函数的适⽤场景:
  • 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动
  • 缩放场景:监控浏览器resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

如何对项目中的图片进行优化?

  1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
  2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
  3. 小图使用 base64 格式
  4. 将多个图标文件整合到一张图片中(雪碧图)
  5. 选择正确的图片格式:
  • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
  • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
  • 照片使用 JPEG

常见的图片格式及使用场景

(1)BMP,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以BMP格式的图片通常是较大的文件。

(2)GIF是无损的、采用索引色的点阵图。采用LZW压缩算法进行编码。文件小,是GIF格式的优点,同时,GIF格式还具有支持动画以及透明的优点。但是GIF格式仅支持8bit的索引色,所以GIF格式适用于对色彩要求不高同时需要文件体积较小的场景。

(3)JPEG是有损的、采用直接色的点阵图。JPEG的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG非常适合用来存储照片,与GIF相比,JPEG不适合用来存储企业Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较GIF更大。

(4)PNG-8是无损的、使用索引色的点阵图。PNG是一种比较新的图片格式,PNG-8是非常好的GIF格式替代者,在可能的情况下,应该尽可能的使用PNG-8而不是GIF,因为在相同的图片效果下,PNG-8具有更小的文件体积。除此之外,PNG-8还支持透明度的调节,而GIF并不支持。除非需要动画的支持,否则没有理由使用GIF而不是PNG-8。

(5)PNG-24是无损的、使用直接色的点阵图。PNG-24的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24格式的文件大小要比BMP小得多。当然,PNG24的图片还是要比JPEG、GIF、PNG-8大得多。

(6)SVG是无损的矢量图。SVG是矢量图意味着SVG图片由直线和曲线以及绘制它们的方法组成。当放大SVG图片时,看到的还是线和曲线,而不会出现像素点。这意味着SVG图片在放大时,不会失真,所以它非常适合用来绘制Logo、Icon等。

(7)WebP是谷歌开发的一种新图片格式,WebP是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为Web而生的,什么叫为Web而生呢?就是说相同质量的图片,WebP具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有Chrome浏览器和Opera浏览器支持WebP格式,兼容性不太好。WebP图片格式支持图片透明度,一个无损压缩的WebP图片,如果要支持透明度只需要22%的格外文件大小。

优化首屏响应

觉得快

loading

vue页面需要通过js构建,因此在js下载到本地之前,页面上什么也没有 一个非常简单有效的办法,即在页面中先渲染一个小的加载中效果,等到js下载到本地并运行后,即会自动替换

nprogress

源码分析地址:https://blog.csdn.net/qq_31968791/article/details/106790179 使用到的库是什么 nprogress 进度条的实现原理知道吗 Nprogress的原理非常简单,就是页面启动的时候,构建一个方法,创建一个div,然后这个div靠近最顶部,用fixed定位住,至于样式就是按照自个或者默认走了。 怎么使用这个库的 主要采用的两个方法是nprogress.start和nprogress.done 如何使用: 在请求拦截器中调用nprogress.start 在响应拦截器中调用nprogress.done

betterScroll

https://blog.csdn.net/weixin_37719279/article/details/82084342 使用的库是什么:better-scroll

骨架屏

骨架屏的原理:https://blog.csdn.net/csdn_yudong/article/details/103909178

你能说说为啥使用骨架屏吗?

现在的前端开发领域,都是前后端分离,前端框架主流的都是 SPA,MPA;这就意味着,页面渲染以及等待的白屏时间,成为我们需要解决的问题点;而且大项目,这个问题尤为突出。 webpack 可以实现按需加载,减小我们首屏需要加载的代码体积;再配合上 CDN 以及一些静态代码(框架,组件库等等…)缓存技术,可以很好的缓解这个加载渲染的时间过长的问题。 但即便如此,首屏的加载依然还是存在这个加载以及渲染的等待时间问题; 现在的前端开发领域,都是前后端分离,前端框架主流的都是 SPA,MPA;这就意味着,页面渲染以及等待的白屏时间,成为我们需要解决的问题点;而且大项目,这个问题尤为突出。 webpack 可以实现按需加载,减小我们首屏需要加载的代码体积;再配合上 CDN 以及一些静态代码(框架,组件库等等…)缓存技术,可以很好的缓解这个加载渲染的时间过长的问题。 目前主流,常见的解决方案是使用骨架屏技术,包括很多原生的APP,在页面渲染时,也会使用骨架屏。(下图中,红圈中的部分,即为骨架屏在内容还没有出现之前的页面骨架填充,以免留白)

骨架屏的要怎么使用呢?骨架屏的原理知道吗?

  1. 在 index.html 中的 div#app 中来实现骨架屏,程序渲染后就会替换掉 index.html 里面的 div#app 骨架屏内容;
  2. 使用一个Base64的图片来作为骨架屏

使用图片作为骨架屏; 简单暴力,让UI同学花点功夫吧;小米商城的移动端页面采用的就是这个方法,它是使用了一个Base64的图片来作为骨架屏。 按照方案一的方案,将这个 Base64 的图片写在我们的 index.html 模块中的 div#app 里面。

  1. 使用 .vue 文件来完成骨架屏

真实快

webpack 怎么进行首屏加载的优化?

  1. CDN 如果工程中使用了一些知名的第三方库,可以考虑使用 CDN,而不进行打包
  2. 抽离公共模块 如果工程中用到了一些大的公共库,可以考虑将其分割出来单独打包
  3. 异步加载 对于那些不需要在一开始就执行的模块,可以考虑使用动态导入的方式异步加载它们,以尽量减少主包的体积
  4. 压缩、混淆
  5. tree shaking 尽量使用 ESM 语法进行导入导出,充分利用 tree shaking 去除无用代码
  6. gzip 开启 gzip 压缩,进一步减少包体积
  7. 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。
- +

浏览器刷新页面的频率 1s 60s

每 16.7mm 刷新一次

gpu 可以再一帧里渲染好页面,那么当你改动页面的元素或者实现动画的时候,将会非常流畅

documentFragment 是什么?用它跟直接操作 DOM 的区别是什么?

MDN 中对 documentFragment 的解释:

DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document 使用,就像标准的 document 一样,存储由节点(nodes)组成的文档结构。与 document 相比,最大的区别是 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。

当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的 DOM 操作时,我们就可以将 DOM 元素插入 DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作 DOM 相比,将 DocumentFragment 节点插入 DOM 树时,不会触发页面的重绘,这样就大大提高了页面的性能。

防抖函数

  • 按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次
  • 服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤ lodash.debounce 节流函数的适⽤场景:
  • 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动
  • 缩放场景:监控浏览器 resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

如何对项目中的图片进行优化?

  1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
  2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
  3. 小图使用 base64 格式
  4. 将多个图标文件整合到一张图片中(雪碧图)
  5. 选择正确的图片格式:
  • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
  • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
  • 照片使用 JPEG

常见的图片格式及使用场景

(1)BMP,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以 BMP 格式的图片通常是较大的文件。

(2)GIF 是无损的、采用索引色的点阵图。采用 LZW 压缩算法进行编码。文件小,是 GIF 格式的优点,同时,GIF 格式还具有支持动画以及透明的优点。但是 GIF 格式仅支持 8bit 的索引色,所以 GIF 格式适用于对色彩要求不高同时需要文件体积较小的场景。

(3)JPEG 是有损的、采用直接色的点阵图。JPEG 的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG 非常适合用来存储照片,与 GIF 相比,JPEG 不适合用来存储企业 Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较 GIF 更大。

(4)PNG-8 是无损的、使用索引色的点阵图。PNG 是一种比较新的图片格式,PNG-8 是非常好的 GIF 格式替代者,在可能的情况下,应该尽可能的使用 PNG-8 而不是 GIF,因为在相同的图片效果下,PNG-8 具有更小的文件体积。除此之外,PNG-8 还支持透明度的调节,而 GIF 并不支持。除非需要动画的支持,否则没有理由使用 GIF 而不是 PNG-8。

(5)PNG-24 是无损的、使用直接色的点阵图。PNG-24 的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24 格式的文件大小要比 BMP 小得多。当然,PNG24 的图片还是要比 JPEG、GIF、PNG-8 大得多。

(6)SVG 是无损的矢量图。SVG 是矢量图意味着 SVG 图片由直线和曲线以及绘制它们的方法组成。当放大 SVG 图片时,看到的还是线和曲线,而不会出现像素点。这意味着 SVG 图片在放大时,不会失真,所以它非常适合用来绘制 Logo、Icon 等。

(7)WebP 是谷歌开发的一种新图片格式,WebP 是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为 Web 而生的,什么叫为 Web 而生呢?就是说相同质量的图片,WebP 具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有 Chrome 浏览器和 Opera 浏览器支持 WebP 格式,兼容性不太好。WebP 图片格式支持图片透明度,一个无损压缩的 WebP 图片,如果要支持透明度只需要 22%的格外文件大小。

优化首屏响应

觉得快

loading

vue 页面需要通过 js 构建,因此在 js 下载到本地之前,页面上什么也没有 一个非常简单有效的办法,即在页面中先渲染一个小的加载中效果,等到 js 下载到本地并运行后,即会自动替换

nprogress

源码分析地址:https://blog.csdn.net/qq_31968791/article/details/106790179 使用到的库是什么 nprogress 进度条的实现原理知道吗 Nprogress 的原理非常简单,就是页面启动的时候,构建一个方法,创建一个 div,然后这个 div 靠近最顶部,用 fixed 定位住,至于样式就是按照自个或者默认走了。 怎么使用这个库的 主要采用的两个方法是 nprogress.start 和 nprogress.done 如何使用: 在请求拦截器中调用 nprogress.start 在响应拦截器中调用 nprogress.done

betterScroll

https://blog.csdn.net/weixin_37719279/article/details/82084342 使用的库是什么:better-scroll

骨架屏

骨架屏的原理:https://blog.csdn.net/csdn_yudong/article/details/103909178

你能说说为啥使用骨架屏吗?

现在的前端开发领域,都是前后端分离,前端框架主流的都是 SPA,MPA;这就意味着,页面渲染以及等待的白屏时间,成为我们需要解决的问题点;而且大项目,这个问题尤为突出。 webpack 可以实现按需加载,减小我们首屏需要加载的代码体积;再配合上 CDN 以及一些静态代码(框架,组件库等等…)缓存技术,可以很好的缓解这个加载渲染的时间过长的问题。 但即便如此,首屏的加载依然还是存在这个加载以及渲染的等待时间问题; 现在的前端开发领域,都是前后端分离,前端框架主流的都是 SPA,MPA;这就意味着,页面渲染以及等待的白屏时间,成为我们需要解决的问题点;而且大项目,这个问题尤为突出。 webpack 可以实现按需加载,减小我们首屏需要加载的代码体积;再配合上 CDN 以及一些静态代码(框架,组件库等等…)缓存技术,可以很好的缓解这个加载渲染的时间过长的问题。 目前主流,常见的解决方案是使用骨架屏技术,包括很多原生的 APP,在页面渲染时,也会使用骨架屏。(下图中,红圈中的部分,即为骨架屏在内容还没有出现之前的页面骨架填充,以免留白)

骨架屏的要怎么使用呢?骨架屏的原理知道吗?

  1. 在 index.html 中的 div#app 中来实现骨架屏,程序渲染后就会替换掉 index.html 里面的 div#app 骨架屏内容;
  2. 使用一个 Base64 的图片来作为骨架屏

使用图片作为骨架屏; 简单暴力,让 UI 同学花点功夫吧;小米商城的移动端页面采用的就是这个方法,它是使用了一个 Base64 的图片来作为骨架屏。 按照方案一的方案,将这个 Base64 的图片写在我们的 index.html 模块中的 div#app 里面。

  1. 使用 .vue 文件来完成骨架屏

真实快

webpack 怎么进行首屏加载的优化?

  1. CDN 如果工程中使用了一些知名的第三方库,可以考虑使用 CDN,而不进行打包
  2. 抽离公共模块 如果工程中用到了一些大的公共库,可以考虑将其分割出来单独打包
  3. 异步加载 对于那些不需要在一开始就执行的模块,可以考虑使用动态导入的方式异步加载它们,以尽量减少主包的体积
  4. 压缩、混淆
  5. tree shaking 尽量使用 ESM 语法进行导入导出,充分利用 tree shaking 去除无用代码
  6. gzip 开启 gzip 压缩,进一步减少包体积
  7. 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。
+ diff --git "a/front-end-engineering/pnpm\345\216\237\347\220\206.html" "b/front-end-engineering/pnpm\345\216\237\347\220\206.html" index 34587bfd..f2baaf1b 100644 --- "a/front-end-engineering/pnpm\345\216\237\347\220\206.html" +++ "b/front-end-engineering/pnpm\345\216\237\347\220\206.html" @@ -23,7 +23,7 @@ # /d表示创建的是目录的符号链接,不写则是文件的符号链接

早期的 windows 系统不支持符号链接,但它提供了一个工具 junction 来达到类似的功能。

5、符号链接和硬链接的区别

  1. 硬链接仅能链接文件,而符号链接可以链接目录
  2. 硬链接在链接完成后仅和文件内容关联,和之前链接的文件没有任何关系。而符号链接始终和之前链接的文件关联,和文件内容不直接相关。

6、快捷方式

快捷方式类似于符号链接,是 windows 系统早期就支持的链接方式。它不仅仅是一个指向其他文件或目录的指针,其中还包含了各种信息:如权限、兼容性启动方式等其他各种属性,由于快捷方式是 windows 系统独有的,在跨平台的应用中一般不会使用。

7、node 环境对硬链接和符号链接的处理

硬链接:

硬链接是一个实实在在的文件,node 不对其做任何特殊处理,也无法区别对待,实际上,node 根本无从知晓该文件是不是一个硬链接

符号链接:

由于符号链接指向的是另一个文件或目录,当 node 执行符号链接下的 JS 文件时,会使用原始路径。比方说:我在 D 盘装了 LOL,在桌面创建了 LOL 快捷方式,相当于是符号链接,双击快捷方式运行游戏,在运行游戏的时候是按照 LOL 原始路径(D 盘路径)运行的。

8、pnpm 原理

pnpm 使用符号链接和硬链接来构建 node_modules 目录

下面用一个例子来说明它的构建方式

假设两个包 a 和 b,a 依赖 b:

假设我们的工程为 proj,直接依赖 a,则安装时,pnpm 会做下面的处理:

  1. 通过 package.json 查询依赖关系,得到最终要安装的包:a 和 b

  2. 在工程 proj 根目录中查看 a 和 b 是否已经有缓存,如果没有,下载到缓存中,如果有,则进入下一步

  3. 在 proj 中创建 node_modules 目录,并对目录进行结构初始化

  4. 从缓存的对应包中使用硬链接放置文件到相应包代码目录中

  5. 使用符号链接,将每个包的直接依赖放置到自己的目录中

    这样做的目的,是为了保证 a 的代码在执行过程中,可以读取到它们的直接依赖

  6. 新版本的 pnpm 为了解决一些书写不规范的包(读取间接依赖)的问题,又将所有的工程非直接依赖,使用符号链接加入到了 .pnpm/node_modules 中。如果 b 依赖 c,a 又要直接用 c,这种不规范的用法现在 pnpm 通过这种方式支持了。但对于那些使用绝对路径的奇葩写法,可能没有办法支持。

  7. 在工程的 node_modules 目录中使用符号链接,放置直接依赖

其他资料

https://juejin.cn/post/6916101419703468045

https://www.bilibili.com/video/BV1tg4y1x75Q/?p=3&spm_id_from=pageDriver&vd_source=5ca956a1f37d0ed72cd1c453c15a3c03

- + diff --git a/front-end-engineering/theme.html b/front-end-engineering/theme.html index e7e79e2d..16d57172 100644 --- a/front-end-engineering/theme.html +++ b/front-end-engineering/theme.html @@ -675,7 +675,7 @@ } }

所有源码均在:https://github.com/Sunny-117/blog/tree/main/code/theme-switch

- + diff --git a/front-end-engineering/webpack5-mf.html b/front-end-engineering/webpack5-mf.html index 2200141c..d387cbf4 100644 --- a/front-end-engineering/webpack5-mf.html +++ b/front-end-engineering/webpack5-mf.html @@ -115,7 +115,7 @@ ] }

webpack会根据需要从合适的位置引入合适的版本

- + diff --git "a/front-end-engineering/webpack\345\270\270\347\224\250\346\213\223\345\261\225.html" "b/front-end-engineering/webpack\345\270\270\347\224\250\346\213\223\345\261\225.html" index c2988ff8..15a8c2fe 100644 --- "a/front-end-engineering/webpack\345\270\270\347\224\250\346\213\223\345\261\225.html" +++ "b/front-end-engineering/webpack\345\270\270\347\224\250\346\213\223\345\261\225.html" @@ -217,7 +217,7 @@
$('#item'); // <= 起作用
 _.drop([1, 2, 3], 2); // <= 起作用
 
- + diff --git "a/front-end-engineering/\343\200\220\345\256\214\347\273\223\343\200\221\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226.html" "b/front-end-engineering/\343\200\220\345\256\214\347\273\223\343\200\221\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226.html" index 488f8c4c..fd816d6e 100644 --- "a/front-end-engineering/\343\200\220\345\256\214\347\273\223\343\200\221\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226.html" +++ "b/front-end-engineering/\343\200\220\345\256\214\347\273\223\343\200\221\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226.html" @@ -477,7 +477,7 @@ # 设置缓存位置 npm config set cache "新的缓存路径" - + diff --git "a/front-end-engineering/\345\220\216\345\244\204\347\220\206\345\231\250.html" "b/front-end-engineering/\345\220\216\345\244\204\347\220\206\345\231\250.html" index c070928a..0e80d84e 100644 --- "a/front-end-engineering/\345\220\216\345\244\204\347\220\206\345\231\250.html" +++ "b/front-end-engineering/\345\220\216\345\244\204\347\220\206\345\231\250.html" @@ -803,7 +803,7 @@ }; module.exports.postcss = true; - + diff --git a/getting-started.html b/getting-started.html index 76650dc5..af6372ca 100644 --- a/getting-started.html +++ b/getting-started.html @@ -13,7 +13,7 @@
Skip to content
On this page

My Projects

TODO:分类

js-challenges

https://github.com/Sunny-117/js-challenges

✨✨✨ Challenge your JavaScript programming limits step by step

mini-anythings

https://github.com/Sunny-117/mini-anything

🚀 Explore the source code of the front-end library and implement a super mini version

BOSScript

https://github.com/Sunny-117/BOSScript

Boss's direct recruitment and delivery, shutdown, one-stop service of the oil monkey script, allowing you to submit resumes overseas in just 2 minutes

rc-design

https://github.com/Sunny-117/rc-design

🗃️ rc-design is a component library developed for react, providing developers with a more lightweight and concise component library choice. Use tsx to write logic, less to write styles, dumi2 to write documentation sites, and jest+ts-jest+react-testing-library for unit testing.

cherry

https://github.com/Sunny-117/cherry

✨ A lightweight JavaScript packaging library based on magic-string and acorn, supporting tree-shaking

rollup-plugin-alias

https://github.com/Sunny-117/rollup-plugin-alias

🍣 A Rollup plugin for defining aliases when bundling packages.

commencer

https://github.com/Sunny-117/commencer

Starter template for xxx

tiny-react

https://github.com/Sunny-117/tiny-react

🌱 The closest implementation to the React source code

treejs

https://github.com/Sunny-117/treejs

🌱 Easy to learn, high-performance, and highly scalable Tree components, supporting Vuejs and React simultaneously

lodash-ts

https://github.com/Sunny-117/lodash-ts

tiny-vue

https://github.com/Sunny-117/tiny-vue

Native-project

https://github.com/Sunny-117/Native-project

Native JavaScript project collection, Github China latest version

shooks

https://github.com/Sunny-117/shooks

📦️ A high-quality & reliable React Hooks library.

mini-webpack

https://github.com/Sunny-117/mini-webpack

手写一个简易版的webpack

ts-lib-vite

https://github.com/Sunny-117/ts-lib-vite

A foundation for developing front-end utility libraries using Vite and TypeScript.

esbuild-plugins

https://github.com/Sunny-117/esbuild-plugins

packages of esbuild plugins

tiny-vite

https://github.com/Sunny-117/tiny-vite

⚡️ a lightweight frontend build tool designed to deliver swift development experiences and efficient build processes

vite-plugins

https://github.com/Sunny-117/vite-plugins

tiny-complier

实现超级 mini 的编译器 | codegen&compiler 生成代码 | 只需要 200 行代码 | 前端编译原理

https://github.com/Sunny-117/tiny-complier

keep-everyday

https://github.com/Sunny-117/keep-everyday

使用 Github Actions 来完成自动创建 issues 任务

text-image

https://github.com/Sunny-117/text-image

🐛🐛🐛 text-image 可以将文字、图片、视频进行「文本化」,只需要通过简单的配置即可使用

jsx-compilation

https://github.com/Sunny-117/jsx-compilation

🍻 实现 JSX 语法转成 JS 语法的编译器

awesome-native

https://github.com/Sunny-117/awesome-native

🔧 Collection of native JavaScript projects

vsc-delete-func

https://github.com/Sunny-117/vsc-delete-func

🍻🍻🍻 vscode plugins

eslint-plugin-reviewget

https://github.com/Sunny-117/eslint-plugin-reviewget

🚀当用户使用 getXXX get开头的函数的时候 如果不返回值的话 那么就会报错 🐛可以 fix 🎉用户可以自行配置是否 fix

babel-plugin-dev-debug

https://github.com/Sunny-117/babel-plugin-dev-debug

an babel plugin that for dev debug

webpack-expand-lib

https://github.com/Sunny-117/webpack-expand-lib

🚀 some expansion libs of webpack

network-speed-js

https://github.com/Sunny-117/network-speed-js

A small tool for testing network speed. It also has the ability to test internal and external networks.

TODO ...

- + diff --git a/hashmap.json b/hashmap.json index 30d29e4a..d9d4b946 100644 --- a/hashmap.json +++ b/hashmap.json @@ -1 +1 @@ -{"fragment_monorepo.md":"7e93c79e","algorithm_🔥刷题之探索最优解.md":"41261772","fe-utils_tool.md":"d7084753","article_cms.md":"1db797e6","fragment_const.md":"5702cb72","fe-utils_js工具库.md":"1e6ddee0","fe-utils_git.md":"fd0526af","fragment_npm-scripts.md":"a5e03266","fragment_disable-debugger.md":"079495f0","fragment_fetch.md":"a8b42ad6","fragment_foreach.md":"a494d026","fragment_fetch-pause.md":"10622b0a","fragment_react-duplicate.md":"d6cd1c3b","fragment_api-no-repeat.md":"ac61adb1","fragment_nexttick.md":"a9446a03","fragment_babel-console.md":"e055d779","fragment_react-hooks-timer.md":"ab1861d9","fragment_return-await.md":"b2bca79d","fragment_promise-cancel.md":"b9256032","fragment_tree-shaking.md":"2f624c45","fragment_auto-try-catch.md":"795f44a9","fragment_settimeout.md":"9d37d7a8","fragment_react-usestate.md":"bd7085a3","fragment_var-array.md":"04a8721e","fragment_userequest.md":"857b6086","fragment_video.md":"32c415c4","fragment_接口设计.md":"7b9e341e","fragment_微内核架构.md":"46d1614d","fragment_沙盒.md":"b3debab8","fragment_黑白.md":"6b2cdd1f","front-end-engineering_packagemanager.md":"3b71662c","front-end-engineering_jscompatibility.md":"8f33e3fb","front-end-engineering_node.md":"e4a62891","front-end-engineering_css工程化.md":"1810f979","front-end-engineering_engineering-onepage.md":"a4ddf299","front-end-engineering_modularization.md":"921ac988","front-end-engineering_pnpm原理.md":"107eb36b","front-end-engineering_webpack5-mf.md":"e4c15103","front-end-engineering_webpack常用拓展.md":"29e1b7a6","front-end-engineering_theme.md":"cf64487f","front-end-engineering_css 预处理器之scss.md":"0d8d09a0","getting-started.md":"b2d7318e","front-end-engineering_【完结】前端工程化.md":"da279999","front-end-engineering_后处理器.md":"d458ef23","front-end-engineering_performance.md":"08504e29","html-css_html.md":"eb3a0589","html-css_animation.md":"c998f03f","html-css_css.md":"9c9d8a32","html-css_canvas-svg.md":"e2ff6624","html-css_drag.md":"cf0ae65e","html-css_flex.md":"da9ee751","html-css_principle.md":"6930dcb0","html-css_temop.md":"233fcb7e","index.md":"06e06ccc","interview_面试官:你还有问题要问我吗.md":"263beb57","interview_算法笔试.md":"ead05e36","html-css_interview.md":"912390e1","html-css_selector.md":"81f437f1","react_fiber.md":"7867436c","js_代理与反射.md":"c9520d6c","react_component-communication.md":"5b932ed8","react_redux.md":"c9163688","react_event.md":"50a277c1","react_dva.md":"9c53349f","react_reactrouter.md":"f795da7a","react_context.md":"b962ca3a","react_hooks.md":"4e1d1f24","js_迭代器和生成器.md":"9c646ab4","js_异步处理.md":"01fba58b","react_lifecycle.md":"c4db6e2c","react_react-redux-router.md":"8ddf584d","react_index.md":"b13cd8d2","react_transition.md":"2fe5688f","react_utils.md":"b40a882d","react_umi.md":"d31b6477","react_render.md":"3fa7b370","vue_ssr.md":"efb28cdd","vue_computed.md":"d70c4c97","vue_component-communication.md":"e806c81b","vue_challages.md":"c6ae4728","vue_keep-alive-lru.md":"a6d0c9a3","vue_interviewer.md":"1e30c648","vue_lifecycle.md":"f4424b89","vue_directive.md":"db07d9e7","vue_diff.md":"f8fe76c6","vue_nexttick.md":"1a3b9c15","react_react-interview.md":"f8db0f8d","vue_slot.md":"0a1f8353","vue_vue-compile.md":"fff35cb6","vue_v-model.md":"5e01b53a","vue_vue-cli.md":"cef17c2f","vue_vdom.md":"a71aa0d1","vue_vs.md":"69aafdac","vue_reactive.md":"0bc25164","vue_vue-interview.md":"feb0c04f","vue_vue-router.md":"e054a826","vue_vuex.md":"72893add","ts_typescript-onepage.md":"5ce7b35d","vue_vue3-onepage.md":"ab0446bc"} +{"algorithm_🔥刷题之探索最优解.md":"41261772","fe-utils_tool.md":"d7084753","article_cms.md":"1db797e6","fragment_const.md":"5702cb72","fragment_monorepo.md":"7e93c79e","fragment_fetch-pause.md":"10622b0a","fragment_npm-scripts.md":"a5e03266","fe-utils_js工具库.md":"1e6ddee0","fe-utils_git.md":"fd0526af","fragment_fetch.md":"a8b42ad6","fragment_foreach.md":"a494d026","fragment_api-no-repeat.md":"ac61adb1","fragment_disable-debugger.md":"079495f0","fragment_nexttick.md":"a9446a03","fragment_react-duplicate.md":"d6cd1c3b","fragment_babel-console.md":"e055d779","fragment_promise-cancel.md":"b9256032","fragment_react-hooks-timer.md":"ab1861d9","fragment_auto-try-catch.md":"795f44a9","fragment_react-usestate.md":"bd7085a3","fragment_return-await.md":"b2bca79d","fragment_settimeout.md":"9d37d7a8","fragment_tree-shaking.md":"2f624c45","fragment_var-array.md":"04a8721e","fragment_userequest.md":"857b6086","fragment_video.md":"32c415c4","fragment_接口设计.md":"7b9e341e","fragment_微内核架构.md":"46d1614d","fragment_沙盒.md":"b3debab8","fragment_黑白.md":"6b2cdd1f","front-end-engineering_css 预处理器之scss.md":"0d8d09a0","front-end-engineering_css工程化.md":"1810f979","front-end-engineering_packagemanager.md":"3b71662c","front-end-engineering_jscompatibility.md":"8f33e3fb","front-end-engineering_engineering-onepage.md":"a4ddf299","front-end-engineering_node.md":"e4a62891","front-end-engineering_modularization.md":"921ac988","front-end-engineering_pnpm原理.md":"107eb36b","front-end-engineering_webpack5-mf.md":"e4c15103","front-end-engineering_webpack常用拓展.md":"29e1b7a6","front-end-engineering_theme.md":"cf64487f","front-end-engineering_【完结】前端工程化.md":"da279999","front-end-engineering_performance.md":"e4e7bf85","getting-started.md":"b2d7318e","front-end-engineering_后处理器.md":"d458ef23","html-css_html.md":"eb3a0589","html-css_css.md":"9c9d8a32","html-css_flex.md":"da9ee751","html-css_drag.md":"cf0ae65e","html-css_canvas-svg.md":"e2ff6624","html-css_animation.md":"c998f03f","html-css_principle.md":"6930dcb0","html-css_temop.md":"233fcb7e","index.md":"06e06ccc","interview_面试官:你还有问题要问我吗.md":"263beb57","interview_算法笔试.md":"ead05e36","html-css_interview.md":"912390e1","html-css_selector.md":"81f437f1","js_代理与反射.md":"c9520d6c","react_fiber.md":"7867436c","js_迭代器和生成器.md":"9c646ab4","react_component-communication.md":"5b932ed8","react_redux.md":"c9163688","js_异步处理.md":"01fba58b","react_dva.md":"9c53349f","react_event.md":"50a277c1","react_hooks.md":"4e1d1f24","react_reactrouter.md":"f795da7a","react_context.md":"b962ca3a","react_react-redux-router.md":"8ddf584d","react_transition.md":"2fe5688f","react_umi.md":"d31b6477","react_utils.md":"b40a882d","react_lifecycle.md":"c4db6e2c","react_render.md":"3fa7b370","react_index.md":"b13cd8d2","vue_ssr.md":"efb28cdd","vue_computed.md":"d70c4c97","vue_directive.md":"db07d9e7","vue_component-communication.md":"e806c81b","vue_interviewer.md":"1e30c648","react_react-interview.md":"f8db0f8d","vue_keep-alive-lru.md":"a6d0c9a3","vue_challages.md":"c6ae4728","vue_lifecycle.md":"f4424b89","vue_diff.md":"f8fe76c6","vue_nexttick.md":"1a3b9c15","vue_slot.md":"0a1f8353","vue_v-model.md":"5e01b53a","vue_vdom.md":"a71aa0d1","vue_vs.md":"69aafdac","vue_vue-cli.md":"cef17c2f","vue_vue-compile.md":"fff35cb6","vue_reactive.md":"0bc25164","vue_vuex.md":"72893add","vue_vue-router.md":"e054a826","vue_vue-interview.md":"feb0c04f","vue_vue3-onepage.md":"ab0446bc","ts_typescript-onepage.md":"5ce7b35d"} diff --git a/html-css/CSS.html b/html-css/CSS.html index 162201a8..4ec24015 100644 --- a/html-css/CSS.html +++ b/html-css/CSS.html @@ -3047,7 +3047,7 @@ 区视口宽高中最大的一边分成100份 vmin 区视口宽高中最小的一边分成100份 css样式引入 媒体查询不占用权重 - + diff --git a/html-css/HTML.html b/html-css/HTML.html index f81d3574..897f1a4f 100644 --- a/html-css/HTML.html +++ b/html-css/HTML.html @@ -663,7 +663,7 @@ this.postMessage(result); };

worker.js 里面可以通过 importScripts("./index.js")引入外部 js 文件

- + diff --git a/html-css/animation.html b/html-css/animation.html index ed3c4da6..3a148e7d 100644 --- a/html-css/animation.html +++ b/html-css/animation.html @@ -1573,7 +1573,7 @@ ---- > paint喷色 (reflow重构) (repaint) 逻辑图(多层矢量图) -----> 实际绘制(栅格化) 不设置就用cpu绘制 google chrome 自动调用 gpu - + diff --git a/html-css/canvas-svg.html b/html-css/canvas-svg.html index 56835b47..fbeb817d 100644 --- a/html-css/canvas-svg.html +++ b/html-css/canvas-svg.html @@ -809,7 +809,7 @@ <line x1="100" y1="100" x2="200" y2="100" class="line1"></line> </svg>

总结:SVG 开发中不太用

- + diff --git a/html-css/drag.html b/html-css/drag.html index bb77a09d..bcdf9c14 100644 --- a/html-css/drag.html +++ b/html-css/drag.html @@ -343,7 +343,7 @@
oDragTarget.ondrop = function (e) { e.dataTransfer.dropEffect =
 "link";//放下时候的效果,只在drop里面设置 }
 

试验不通过??

- + diff --git a/html-css/flex.html b/html-css/flex.html index bafc967f..e5d9be7d 100644 --- a/html-css/flex.html +++ b/html-css/flex.html @@ -153,7 +153,7 @@ } </style>

虽然都是充分分配容器的尺寸,但是 flex:1 的尺寸表现更为内敛(优先牺牲自己的尺寸), flex:auto 的尺寸表现则更为霸道(优先扩展自己的尺寸)。

适合使用 flex:1 的场景

当希望元素充分利用剩余空间,同时不会侵占其他元素应有的宽度的时候,适合使用 flex:1 ,这样的场景在 Flex 布局中非常的多。

例如所有的等分列表,或者等比例列表都适合使用 flex:1 或者其他 flex 数值,适合的布局效果轮廓如下图所示。

适合使用 flex:auto 的场景

当希望元素充分利用剩余空间,但是各自的尺寸按照各自内容进行分配的时候,适合使用 flex:auto 。例如导航栏。整体设置为 200px,内部设置 flex:auto,会自动按照内容比例进行分配宽度。

- + diff --git a/html-css/interview.html b/html-css/interview.html index 9ed78de2..87d61cb2 100644 --- a/html-css/interview.html +++ b/html-css/interview.html @@ -2507,7 +2507,7 @@ margin: 150px top: 250px

CSS 中 px、em、rem 的区别

px:绝对单位,页面按精确像素显示; em:相对单位,基准点为父节点字体的大小; rem:相对单位,可理解为“root em”,相对根结点 html 的字体大小来计算,CSS3 新属性;

div 设置成 inline-block,有空隙是什么原因呢?如何解决

https://juejin.cn/post/6991762098111905806

css 中 z-index 属性考察

问:box1 在 box2 上面,还是 box2 在 box1 上面? 答案:box1 在 box2 上面 考察是否对z-index生效条件了解,概念: z-index 只对定位元素有效,这里的定位指:absolute、relative、fixed 和 inherit,其中 inherit 取决于父元素,如果父元素没有设置定位(absolute、relative、fixed)则 z-index 无效,注意低版本 IE 浏览器不支持这个值 除了给出答案,还了解 z-index 属性其他注意点,浏览器兼容性等 4. 4.0 分:

css3 GPU 加速

css3 中 2d 变化和 3d 变化有什么区别,性能对比如何,是否用到了 gpu 加速?

  1. 对于 translate 和 scale,2d 变换时有两条轴,对于 rotate,2d 变化时只有一条轴。3d 变化时,都存在相对 x、y、z 轴的变化。
  2. 2d 和 3d 变化都用到了 gpu,因为 gpu 擅长图像的变化操作
  3. 3d 变化性能更好,虽然 2d 和 3d 都用到了 gpu,但是 2d 变换的元素只有在动画过程中才会提到复合层(composite layer),而 3d 变换的元素一开始就被提到了符合层。

请问 HTML 中如何通过<script>加载 JAVAScript 脚本?如果同步阻塞加载,脚本过大影响页面渲染,如何优化?

默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到

回答出浏览器同步加载,渲染引擎遇到 script 标签停止渲染,等待脚本加载执行完成,回答出脚本加载执行加载时机,通过(defer 或 async)异步加载方式优化防止渲染阻塞

如何 html 中开启和关闭 DNS 预读取

在文档中使用值为 http-equiv 的   标签:可以通过将 content 的参数设置为“on”来改变设置 然后通过使用 rel 属性值为 link type 中的 dns-prefetch 的   标签来对特定域名进行预读取: 总分 4 分,不知道不得分,知道通过 meta 值设置给 2 分, meta 写正确+1 分,link 写正确+1

要求:知道 dns 预读取和预读取的意义。如何开启 dns 预读取

请简述 transform 原理

transform 同时设置多个值,比如 translate(30px,0px) rotate(45deg)和 rotate(45deg) translate(30px,0px)效果是否相同,为什么?不相同 两个变换是有先后顺序的,是先转后移还是先移后转,比较容易比划出来 从原理上来说,变换最后是用矩阵乘向量来运算的,矩阵乘法不具有交换律,因此 M1_M2 和 M2_M1 的结果是不同的

答对了,原理也说出来了,为什么要用矩阵来实现变换也说得出来

如何解决 1px 问题

核心思路: 在 web 中,浏览器为我们提供了 window.devicePixelRatio 来帮助我们获取 dpr。在 css 中,可以使用媒体查询 min-device-pixel-ratio,区分 dpr 我们根据这个像素比,来算出他对应应该有的大小,但是暴露个非常大的兼容问题

解决过程会出现什么问题呢?

说说什么是视口吧(viewport)

viewport 即视窗、视口,用于显示网页部分的区域,在 PC 端视口即是浏览器窗口区域,在移动端,为了让页面展示更多的内容,视窗的宽度默认不为设备的宽度,在移动端视窗有三个概念:布局视窗、视觉视窗、理想视窗

移动端视口配置怎么配的?

移动端适配有哪些方案知道吗?

1、rem 布局 2、vw、vh 布局 3、媒体查询响应式布局

说说媒体查询吧?

通过媒体查询,可以针对不同的屏幕进行单独设置,但是针对所有的屏幕尺寸做适配显然是不合理的,但是可以用来处理极端情况(例如 IPad 大屏设备)或做简单的适配(隐藏元素或改变元素位置)

说说 rem 适配吧

rem 是 CSS3 新增的一个相对单位,这个单位引起了广泛关注。这个单位与 em 有什么区别呢?区别在于使用 rem 为元素设定字体大小时,仍然是相对大小,但相对的只是 HTML 根元素。这个单位可谓集相对大小和绝对大小的优点于一身,通过它既可以做到只修改根元素就成比例地调整所有字体大小,又可以避免字体大小逐层复合的连锁反应。目前,除了 IE8 及更早版本外,所有浏览器均已支持 rem。对于不支持它的浏览器,应对方法也很简单,就是多写一个绝对单位的声明。这些浏览器会忽略用 rem 设定的字体大小

rem 的具体适配方案知道吗?

flexible.js 适配:阿里早期开源的一个移动端适配解决方案

因为当年 viewport 在低版本安卓设备上还有兼容问题,而 vw,vh 还没能实现所有浏览器兼容,所以 flexible 方案用 rem 来模拟 vmin 来实现在不同设备等比缩放的“通用”方案,之所以说是通用方案,是因为他这个方案是根据设备大小去判断页面的展示空间大小即屏幕大小,然后根据屏幕大小去百分百还原设计稿,从而让人看到的效果(展示范围)是一样的,这样一来,苹果 5 和苹果 6p 屏幕如果你按照设计稿还原的话,字体大小实际上不一样,而人们在一样的距离上希望看到的大小其实是一样的,本质上,用户使用更大的屏幕,是想看到更多的内容,而不是更大的字

rem 的弊端知道吗

弊端之一:和根元素 font-size 值强耦合,系统字体放大或缩小时,会导致布局错乱 弊端之二:html 文件头部需插入一段 js 代码

说说 vw/vh 适配

vh、vw 方案即将视觉视口宽度 window.innerWidth 和视觉视口高度 window.innerHeight 等分为 100 份

vw 和 vh 有啥不足吗?

vw 和 vh 的兼容性: Android 4.4 之下和 iOS 8 以下的版本有一定的兼容性问题(但是目前这两版本已经很少有人使用了) rem 的兼容性:

- + diff --git a/html-css/principle.html b/html-css/principle.html index b6ea8645..1a7e1ed3 100644 --- a/html-css/principle.html +++ b/html-css/principle.html @@ -43,7 +43,7 @@ /*红*/ }

像素:--->红绿蓝像点----->空间混色

最小的单位:像点。像素由 3 个像点构成。

空间混色法应用

crt 显示屏

lcd 液晶屏

点距:crt 显示屏求点距的方法的意义,是几乎所有屏幕都通用的

像素的大小:点距

物理像素:设备出厂时,像素的大小

dpi:1 英寸所能容纳的像素点数

1 英寸= 2.54cm

dpi 打印机在一英寸屏幕里面可以打印多少墨点

ppi 一英寸所能容纳的像素点数(点距数)

参照像素

96dpi 一臂之遥的视角去看,显示出的具体大小

标杆 1/96*英寸

css 像素=逻辑像素

设备像素比 dpr = 物理像素/css 像素

衡量屏幕好不好:不看分辨率(分辨率:固定宽高下,展示的像素点数)

看的是 dpi

- + diff --git a/html-css/selector.html b/html-css/selector.html index 2b79130a..d4a9a4e2 100644 --- a/html-css/selector.html +++ b/html-css/selector.html @@ -1329,7 +1329,7 @@ </body> </html> - + diff --git a/html-css/temop.html b/html-css/temop.html index 829db9c4..af4237c8 100644 --- a/html-css/temop.html +++ b/html-css/temop.html @@ -13,7 +13,7 @@
Skip to content
On this page
- + diff --git a/index.html b/index.html index c0666677..e93de85d 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,7 @@
Skip to content

Sunny's blog

前端历险记

前端历险记
- + diff --git "a/interview/\347\256\227\346\263\225\347\254\224\350\257\225.html" "b/interview/\347\256\227\346\263\225\347\254\224\350\257\225.html" index 868d171a..76ff40f9 100644 --- "a/interview/\347\256\227\346\263\225\347\254\224\350\257\225.html" +++ "b/interview/\347\256\227\346\263\225\347\254\224\350\257\225.html" @@ -299,7 +299,7 @@ let line = gets(10000).trim(); print(line.length);

trim()

- + diff --git "a/interview/\351\235\242\350\257\225\345\256\230\357\274\232\344\275\240\350\277\230\346\234\211\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221\345\220\227.html" "b/interview/\351\235\242\350\257\225\345\256\230\357\274\232\344\275\240\350\277\230\346\234\211\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221\345\220\227.html" index 38228c78..c2de61c7 100644 --- "a/interview/\351\235\242\350\257\225\345\256\230\357\274\232\344\275\240\350\277\230\346\234\211\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221\345\220\227.html" +++ "b/interview/\351\235\242\350\257\225\345\256\230\357\274\232\344\275\240\350\277\230\346\234\211\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221\345\220\227.html" @@ -13,7 +13,7 @@
Skip to content
On this page

面试官:你还有问题要问我吗?

下面列表里的问题对于参加技术面试的人来说可能有些用。

列表里的问题并不一定适用于某个特定的职位或者工作类型,也没有排序

最开始的时候这只是我自己的问题列表,但是慢慢地添加了一些我觉得可能让我对这家公司亮红牌的问题。

我也注意到被我面试的人提问我的问题太少了,感觉他们挺浪费机会的。

预期使用方式

  • 检查一下哪些问题你感兴趣
  • 检查一下哪些是你可以自己在网上找到答案的
  • 找不到的话就向面试官提问

绝对不要想把这个列表里的每个问题都问一遍。(尊重面试官的时间,而且你可以通过查找已经发布的答案来显示 你的主动性)

请记住事情总是灵活的,组织的结构调整也会经常发生。拥有一个 bug 追踪系统并不会保证高效处理 bug。 CI/CD (持续集成系统) 也不一定保证交付时间会很短。

职责

  • On-call (电话值班)的计划或者规定是什么?值班或者遇到问题加班时候有加班费吗?
  • 我的日常工作是什么?
  • 有给我设定的特定目标吗?
  • 团队里面初级和高级工程师的比例是多少?(有计划改变吗)
  • 入职培训 (onboarding) 会是什么样的?
  • 每个开发者有多大的自由来做出决定?
  • 在你看来,这个工作做到什么程度算成功?
  • 你期望我在最初的一个月 / 三个月能够完成什么?
  • 试用期结束的时候,你会怎么样衡量我的绩效?
  • 自己单独的开发活动和按部就班工作的比例大概是怎样的?
  • 一个典型的一天或者一周的工作是怎样安排的?
  • 对我的申请你有什么疑虑么?
  • 在这份工作上,我将会和谁紧密合作?
  • 我的直接上级他们的上级都是什么样的管理风格?(事无巨细还是着眼宏观)
  • 我在这个岗位上应该如何发展?会有哪些机会?
  • 每天预期 / 核心工作时间是多少小时?
  • 我入职的岗位是新增还是接替之前离职的同事?(是否有技术债需要还)?(zh)
  • 入职之后在哪个项目组,项目是新成立还是已有的?(zh)

技术

  • 公司常用的技术栈是什么?
  • 你们怎么使用源码控制系统?
  • 你们怎么测试代码?
  • 你们怎么追踪 bug?
  • 你们怎样监控项目?
  • 你们怎么集成和部署代码改动?是使用持续集成和持续部署吗 (CI/CD)?
  • 你们的基础设施搭建在版本管理系统里吗?或者是代码化的吗?
  • 从计划到完成一项任务的工作流是什么样的?
  • 你们如何准备故障恢复?
  • 有标准的开发环境吗?是强制的吗?
  • 你们需要花费多长时间来给产品搭建一个本地测试环境?(分钟 / 小时 / 天)
  • 你们需要花费多长时间来响应代码或者依赖中的安全问题?
  • 所有的开发者都可以使用他们电脑的本地管理员权限吗?
  • 介绍一下你们的技术原则或者展望。
  • 你们的代码有开发文档吗?有没有单独的供消费者阅读的文档?
  • 你们有更高层次的文档吗?比如说 ER 图,数据库范式
  • 你们使用静态代码分析吗?
  • 你们如何管理内部和外部的数字资产?
  • 你们如何管理依赖?
  • 公司是否有技术分享交流活动?有的话,多久一次呢?(zh)
  • 你们的数据库是怎么进行版本控制的?(zh)
  • 业务需求有没有文档记录?是如何记录的?(zh)

团队

  • 工作是怎么组织的?
  • 团队内 / 团队间的交流通常是怎样的?
  • 你们使用什么工具来做项目组织?你的实际体会是什么?
  • 如果遇到不同的意见怎样处理?
  • 谁来设定优先级 / 计划?
  • 如果团队没能赶上预期发布日期怎么办?
  • 每周都会开什么类型的会议?
  • 会有定期的和上级的一对一谈话吗?
  • 产品 / 服务的规划是什么样的?(n 周一发布 / 持续部署 / 多个发布流 / ...)
  • 生产环境发生事故了怎么办?是否有不批评人而分析问题的文化?
  • 有没有一些团队正在经历还尚待解决的挑战?
  • 你们如何跟踪进度?
  • 预期和目标是如何设定的?谁来设定?
  • Code Review 如何实施?
  • 给我介绍下团队里一个典型的 sprint
  • 你们如何平衡技术和商业目标?
  • 你们如何共享知识?
  • 团队有多大?
  • 公司技术团队的架构和人员组成?(zh)
  • 团队内开发、产品、运营哪一方是需求的主要提出方?哪一方更强势?(zh)

问未来的同事

  • 开发者倾向于从哪里学习?
  • 你对在这里工作最满意的地方是?
  • 最不满意的呢?
  • 如果可以的话,你想改变哪里?
  • 团队最老的成员在这里多久了?
  • 在小团队中,有没有出现成员性格互相冲突的情况?最后是如何解决的?

公司

  • 公司为什么在招人?(产品发展 / 新产品 / 波动...)
  • 有没有会议 / 旅行预算?使用的规定是什么?
  • 晋升流程是怎样的?要求 / 预期是怎样沟通的?
  • 绩效评估流程是怎样的?
  • 技术和管理两条职业路径是分开的吗?
  • 对于多元化招聘的现状或者观点是什么?
  • 有公司级别的学习资源吗?比如电子书订阅或者在线课程?
  • 有获取证书的预算吗?
  • 公司的成熟度如何?(早期寻找方向 / 有内容的工作 / 维护中 / ...)
  • 我可以为开源项目做贡献吗?是否需要审批?
  • 你认为公司未来五年或者十年会发展成什么样子?
  • 公司的大多数员工是如何看待整洁代码的?
  • 你上次注意到有人成长是什么时候?他们在哪方面成长了?
  • 在这里成功的定义是什么?如何衡量成功?
  • 有体育活动或者团建么?
  • 有内部的黑客马拉松活动吗?
  • 公司支持开源项目吗?
  • 有竞业限制或者保密协议需要签吗?
  • 你们认为公司文化中的空白是什么?
  • 能够跟我说一公司处于不良情况,以及如何处理的故事吗?
  • 您在这工作了多久了?您觉得体验如何?(zh)
  • 大家为什么会喜欢这里?(zh)
  • 公司的调薪制度是如何的?(zh)

社会问题

  • 你们关于多元化招聘什么看法?
  • 你们的公司文化如何?你认为有什么空白么?
  • 这里的工作生活平衡地怎么样?
  • 公司对气候变化有什么态度吗?

冲突

  • 不同的意见如何处理?
  • 如果被退回了会怎样?(“这个在预计的时间内做不完”)
  • 当团队有压力并且在超负荷工作的时候怎么处理?
  • 如果有人注意到了在流程或者技术等其他方面又改进的地方,怎么办?
  • 当管理层的预期和工程师的绩效之间有差距的时候如何处理?
  • 能给我讲一个公司深处有毒环境以及如何处理的故事吗?
  • 如果在公司内你的同事因涉嫌性侵犯他人而被调查,请问你会如何处理?
  • 假设我自己很不幸是在公司内被性侵的受害者,在公司内部有没有争取合法权益的渠道?

商业

  • 你们现在盈利吗?
  • 如果没有的话,还需要多久?
  • 公司的资金来源是什么?谁影响或者制定高层计划或方向?
  • 你们如何挣钱?
  • 什么阻止了你们挣更多的钱?
  • 公司未来一年的增长计划怎样?五年呢?
  • 你们认为什么是你们的竞争优势?
  • 你们的竞争优势是什么?
  • 公司未来的商业规划是怎样的?有上市的计划吗?(zh)

远程工作

  • 远程工作和办公室工作的比例是多少?
  • 公司提供硬件吗?更新计划如何?
  • 使用自己的硬件办公可以吗?现在有政策吗?
  • 额外的附件和家具可以通过公司购买吗?这方面是否有预算?
  • 有共享办公或者上网的预算吗?
  • 多久需要去一次办公室?
  • 公司的会议室是否一直是视频会议就绪的?

办公室布局

  • 办公室的布局如何?(开放的 / 小隔间 / 独立办公室)
  • 有没有支持 / 市场 / 或者其他需要大量打电话的团队在我的团队旁边办公?

终极问题

  • 该职位为何会空缺?
  • 公司如何保证人才不流失?
  • 这份工作 / 团队 / 公司最好和最坏的方面是?
  • 你最开始为什么选择了这家公司?
  • 你为什么留在这家公司?

待遇

  • 如果有奖金计划的话,奖金如何分配?
  • 如果有奖金计划的话,过去的几年里通常会发百分之多少的奖金?
  • 有五险一金(zh)/401k(us)或者其他退休养老金等福利吗?
  • 五险一金中,补充公积金一般交多少比例?/401k一般交多少比例?我可以自己选择这一比例吗?
  • 有什么医疗保险吗?如果有的话何时开始?
  • 有额外商业保险吗?例如人寿保险和额外的养老/医疗保险?
  • 更换工作地点,公司付费吗?

休假

  • 带薪休假时间有多久?
  • 病假和事假是分开的还是一起算?
  • 我可以提前使用假期时间吗?也就是说应休假期是负的?
  • 假期的更新策略是什么样的?也就是说未休的假期能否滚入下一周期
  • 照顾小孩的政策如何?
  • 无薪休假政策是什么样的?
  • 学术性休假政策是怎么样的?

参考链接

Find more inspiration for questions in:

https://github.com/viraptor/reverse-interview

翻译:

English

Korean

Portuguese

繁體中文

- + diff --git "a/js/\344\273\243\347\220\206\344\270\216\345\217\215\345\260\204.html" "b/js/\344\273\243\347\220\206\344\270\216\345\217\215\345\260\204.html" index 0d6e2b62..cc19d3d4 100644 --- "a/js/\344\273\243\347\220\206\344\270\216\345\217\215\345\260\204.html" +++ "b/js/\344\273\243\347\220\206\344\270\216\345\217\215\345\260\204.html" @@ -973,7 +973,7 @@ const sumProxy = validatorFunction(sum, "number", "number"); console.log(sumProxy(1, 2));

其他 proxy 资料:

https://cloud.tencent.com/developer/article/1890562

https://juejin.cn/post/6844904101218631694

- + diff --git "a/js/\345\274\202\346\255\245\345\244\204\347\220\206.html" "b/js/\345\274\202\346\255\245\345\244\204\347\220\206.html" index 7619968e..73f10335 100644 --- "a/js/\345\274\202\346\255\245\345\244\204\347\220\206.html" +++ "b/js/\345\274\202\346\255\245\345\244\204\347\220\206.html" @@ -2129,7 +2129,7 @@ }); console.log('script end'); - + diff --git "a/js/\350\277\255\344\273\243\345\231\250\345\222\214\347\224\237\346\210\220\345\231\250.html" "b/js/\350\277\255\344\273\243\345\231\250\345\222\214\347\224\237\346\210\220\345\231\250.html" index f59a78f8..bfbc6b35 100644 --- "a/js/\350\277\255\344\273\243\345\231\250\345\222\214\347\224\237\346\210\220\345\231\250.html" +++ "b/js/\350\277\255\344\273\243\345\231\250\345\222\214\347\224\237\346\210\220\345\231\250.html" @@ -1241,7 +1241,7 @@ } } - + diff --git a/react/Fiber.html b/react/Fiber.html index 5936a019..35334bc0 100644 --- a/react/Fiber.html +++ b/react/Fiber.html @@ -13,7 +13,7 @@
Skip to content
On this page

Fiber

React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿

为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。

所以 React 通过 Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:

  • 分批延时对 DOM 进行操作,避免一次性操作大量 DOM 节点,可以得到更好的用户体验;
  • 给浏览器一点喘息的机会,它会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。

核心思想: Fiber 也称协程或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。

- + diff --git a/react/ReactRouter.html b/react/ReactRouter.html index 08f6c50f..fe607838 100644 --- a/react/ReactRouter.html +++ b/react/ReactRouter.html @@ -621,7 +621,7 @@ o 支持监听action的分发,更新状态(dispatch(action)); o 支持订阅store的变更(subscribe(listener));

Mobx 是一个透明函数响应式编程的状态管理库,它使得状态管理简单可伸缩 ∶

对比总结:

Redux 和 Vuex 有什么区别,它们的共同思想

(1)Redux 和 Vuex 区别

通俗点理解就是,vuex 弱化 dispatch,通过 commit 进行 store 状态的一次更变;取消了 action 概念,不必传入特定的 action 形式进行指定变更;弱化 reducer,基于 commit 参数直接对数据进行转变,使得框架更加简易;

(2)共同思想

本质上 ∶ redux 与 vuex 都是对 mvvm 思想的服务,将数据从视图中抽离的一种方案。

Redux 中间件是怎么拿到 store 和 action? 然后怎么处理?

redux 中间件本质就是一个函数柯里化。redux applyMiddleware Api 源码中每个 middleware 接受 2 个参数, Store 的 getState 函数和 dispatch 函数,分别获得 store 和 action,最终返回一个函数。该函数会被传入 next 的下一个 middleware 的 dispatch 方法,并返回一个接收 action 的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store 的 dispatch 方法作为 next 参数,并借此结束调用链。所以,middleware 的函数签名是({ getState,dispatch })=> next => action。

Redux 中的 connect 有什么作用

connect 负责连接 React 和 Redux

(1)获取 state

connect 通过 context 获取 Provider 中的 store,通过store.getState() 获取整个 store tree 上所有 state

(2)包装原组件

将 state 和 action 通过 props 的方式传入到原组件内部 wrapWithConnect 返回—个 ReactComponent 对 象 Connect,Connect 重 新 render 外部传入的原组件 WrappedComponent ,并把 connect 中传入的 mapStateToProps,mapDispatchToProps 与组件上原有的 props 合并后,通过属性的方式传给 WrappedComponent

(3)监听 store tree 变化

connect 缓存了 store tree 中 state 的状态,通过当前 state 状态 和变更前 state 状态进行比较,从而确定是否调用 this.setState()方法触发 Connect 及其子组件的重新渲染

- + diff --git a/react/Redux.html b/react/Redux.html index e85c134f..09e55141 100644 --- a/react/Redux.html +++ b/react/Redux.html @@ -51,7 +51,7 @@ payload: 1, // 用户id为1 };

Redux

在 Flux 基础上,引入了 reducer 的概念

reducer:处理器,用于根据 action 来处理数据,处理后的数据会被仓库重新保存。

Action

  1. action 是一个 plain-object(平面对象)
    1. 它的proto指向 Object.prototype
  2. 通常,使用 payload 属性表示附加数据(没有强制要求)
  3. action 中必须有 type 属性,该属性用于描述操作的类型
    1. 但是,没有对 type 的类型做出要求
  4. 在大型项目,由于操作类型非常多,为了避免硬编码(hard code),会将 action 的类型存放到一个或一些单独的文件中(样板代码)。

  1. 为了方面传递 action,通常会使用 action 创建函数(action creator)来创建 action

  2. action 创建函数应为无副作用的纯函数

    1. 不能以任何形式改动参数
    2. 不可以有异步
    3. 不可以对外部环境中的数据造成影响
  3. 为了方便利用 action 创建函数来分发(触发)action,redux 提供了一个函数bindActionCreators,该函数用于增强 action 创建函数的功能,使它不仅可以创建 action,并且创建后会自动完成分发。

Reducer

Reducer 是用于改变数据的函数

  1. 一个数据仓库,有且仅有一个 reducer,并且通常情况下,一个工程只有一个仓库,因此,一个系统,只有一个 reducer
  2. 为了方便管理,通常会将 reducer 放到单独的文件中。
  3. reducer 被调用的时机
    1. 通过 store.dispatch,分发了一个 action,此时,会调用 reducer
    2. 当创建一个 store 的时候,会调用一次 reducer
      1. 可以利用这一点,用 reducer 初始化状态
      2. 创建仓库时,不传递任何默认状态
      3. 将 reducer 的参数 state 设置一个默认值。创建仓库不写默认值,传递 reducer 的时候传递默认值
  4. reducer 内部通常使用 switch 来判断 type 值
  5. reducer 必须是一个没有副作用的纯函数
    1. 为什么需要纯函数
      1. 纯函数有利于测试和调式
      2. 有利于还原数据
      3. 有利于将来和 react 结合时的优化
    2. 具体要求
      1. 不能改变参数,因此若要让状态变化,必须得到一个新的状态
      2. 不能有异步
      3. 不能对外部环境造成影响
  6. 由于在大中型项目中,操作比较复杂,数据结构也比较复杂,因此,需要对 reducer 进行细分。
    1. redux 提供了方法,可以帮助我们更加方便的合并 reducer
    2. combineReducers: 合并 reducer,得到一个新的 reducer,该新的 reducer 管理一个对象,该对象中的每一个属性交给对应的 reducer 管理。

Store

Store:用于保存数据

通过 createStore 方法创建的对象。

该对象的成员:

createStore

返回一个对象:

bindActionCreators

combineReducers

组装 reducers,返回一个 reducer,数据使用一个对象表示,对象的属性名与传递的参数对象保持一致

Redux 中间件(Middleware)

中间件:类似于插件,可以在不影响原本功能、并且不改动原本代码的基础上,对其功能进行增强。在 Redux 中,中间件主要用于增强 dispatch 函数。

实现 Redux 中间件的基本原理,是更改仓库中的 dispatch 函数。

Redux 中间件书写:

redux-actions

不维护了:https://github.com/redux-utilities/redux-actions#looking-for-maintainers

该库用于简化 action-types、action-creator 以及 reducer 官网文档:https://redux-actions.js.org/

createAction(s)

createAction

该函数用于帮助你创建一个 action 创建函数(action creator)

createActions

该函数用于帮助你创建多个 action 创建函数

handleAction(s)

handleAction

简化针对单个 action 类型的 reducer 处理,当它匹配到对应的 action 类型后,会执行对应的函数

handleActions

简化针对多个 action 类型的 reducre 处理

combineActions

配合 createActions 和 handleActions 两个函数,用于处理多个 action-type 对应同一个 reducer 处理函数。

- + diff --git a/react/component-communication.html b/react/component-communication.html index d3647a5c..e86c71dc 100644 --- a/react/component-communication.html +++ b/react/component-communication.html @@ -13,7 +13,7 @@
Skip to content
On this page

React 组件通信

1. 父子组件的通信方式?

父组件向子组件通信:父组件通过 props 向子组件传递需要的信息。

子组件向父组件通信:: props+回调的方式。

2. 跨级组件的通信方式?

父组件向子组件的子组件通信,向更深层子组件通信:

  • 使用 props,利用中间组件层层传递,但是如果父组件结构较深,那么中间每一层组件都要去传递 props,增加了复杂度,并且这些 props 并不是中间组件自己需要的。
  • 使用 context,context 相当于一个大容器,可以把要通信的内容放在这个容器中,这样不管嵌套多深,都可以随意取用,对于跨越多层的全局数据可以使用 context 实现。

3. 非嵌套关系组件的通信方式?

即没有任何包含关系的组件,包括兄弟组件以及不在同一个父级中的非兄弟组件。

  • 可以使用自定义事件通信(发布订阅模式)
  • 可以通过 redux 等进行全局状态管理
  • 如果是兄弟组件通信,可以找到这两个兄弟节点共同的父节点, 结合父子间通信方式进行通信。
- + diff --git a/react/context.html b/react/context.html index 2368dd96..9f6f1d68 100644 --- a/react/context.html +++ b/react/context.html @@ -811,7 +811,7 @@ ); }

对 React context 的理解

在 React 中,数据传递一般使用 props 传递数据,维持单向数据流,这样可以让组件之间的关系变得简单且可预测,但是单项数据流在某些场景中并不适用。单纯一对的父子组件传递并无问题,但要是组件之间层层依赖深入,props 就需要层层传递显然,这样做太繁琐了。

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

可以把 context 当做是特定一个组件树内共享的 store,用来做数据传递。简单说就是,当你不想在组件树中通过逐层传递 props 或者 state 的方式来传递数据时,可以使用 Context 来实现跨层级的组件数据传递。

JS 的代码块在执行期间,会创建一个相应的作用域链,这个作用域链记录着运行时 JS 代码块执行期间所能访问的活动对象,包括变量和函数,JS 程序通过作用域链访问到代码块内部或者外部的变量和函数。

假如以 JS 的作用域链作为类比,React 组件提供的 Context 对象其实就好比一个提供给子组件访问的作用域,而 Context 对象的属性可以看成作用域上的活动对象。由于组件 的 Context 由其父节点链上所有组件通 过 getChildContext()返回的 Context 对象组合而成,所以,组件通过 Context 是可以访问到其父组件链上所有节点组件提供的 Context 的属性。

为什么 React 并不推荐优先考虑使用 Context?

- + diff --git a/react/dva.html b/react/dva.html index 6649366a..eb955e8c 100644 --- a/react/dva.html +++ b/react/dva.html @@ -13,7 +13,7 @@
Skip to content
On this page

dva

DANGER

已经不维护了,仅供学习

官方网站:https://dvajs.com dva 不仅仅是一个第三方库,更是一个框架,它主要整合了 redux 的相关内容,让我们处理数据更加容易,实际上,dva 依赖了很多:react、react-router、redux、redux-saga、react-redux、connected-react-router 等。

dva 的使用

  1. dva 默认导出一个函数,通过调用该函数,可以得到一个 dva 对象
  2. dva 对象.router:路由方法,传入一个函数,该函数返回一个 React 节点,将来,应用程序启动后,会自动渲染该节点。
  3. dva 对象.start: 该方法用于启动 dva 应用程序,可以认为启动的就是 react 程序,该函数传入一个选择器,用于选中页面中的某个 dom 元素,react 会将内容渲染到该元素内部。
  4. dva 对象.model: 该方法用于定义一个模型,该模型可以理解为 redux 的 action、reducer、redux-saga 副作用处理的整合,整合成一个对象,将该对象传入 model 方法即可。
  5. namespace:命名空间,该属性是一个字符串,字符串的值,会被作为仓库中的属性保存
  6. state:该模型的默认状态
  7. reducers: 该属性配置为一个对象,对象中的每个方法就是一个 reducer,dva 约定,方法的名字,就是匹配的 action 类型
  8. effects: 处理副作用,底层是使用 redux-saga 实现的,该属性配置为一个对象,对象中的每隔方法均处理一个副作用,方法的名字,就是匹配的 action 类型。
    1. 函数的参数 1:action
    2. 参数 2:封装好的 saga/effects 对象
  9. subscriptions:配置为一个对象,该对象中可以写任意数量任意名称的属性,每个属性是一个函数,这些函数会在模型加入到仓库中后立即运行。
  10. 在 dva 中同步路由到仓库
  11. 在调用 dva 函数时,配置 history 对象
  12. 使用 ConnectedRouter 提供路由上下文
  13. 配置:
  14. history:同步到仓库的 history 对象
  15. initialState:创建 redux 仓库时,使用的默认状态
  16. onError: 当仓库的运行发生错误的时候,运行的函数
  17. onAction: 可以配置 redux 中间件
    1. 传入一个中间件对象
    2. 传入一个中间件数组
  18. onStateChange: 当仓库中的状态发生变化时运行的函数
  19. onReducer:对模型中的 reducer 的进一步封装
  20. onEffect:类似于对模型中的 effect 的进一步封装
  21. extraReducers:用于配置额外的 reducer,它是一个对象,对象的每一个属性是一个方法,每个方法就是一个需要合并的 reducer,方法名即属性名。
  22. extraEnhancers: 它是用于封装 createStore 函数的,dva 会将原来的仓库创建函数作为参数传递,返回一个新的用于创建仓库的函数。函数必须放置到数组中。

dva 插件

通过dva对象.use(插件),来使用插件,插件本质上就是一个对象,该对象与配置对象相同,dva 会在启动时,将传递的插件对象混合到配置中。

dva-loading

该插件会在仓库中加入一个状态,名称为 loading,它是一个对象,其中有以下属性

  • global:全局是否正在处理副作用(加载),只要有任何一个模型在处理副作用,则该属性为 true
  • models:一个对象,对象中的属性名以及属性的值,表示哪个对应的模型是否在处理副作用中(加载中)
  • effects:一个对象,对象中的属性名以及属性的值,表示是哪个 action 触发了副作用
- + diff --git a/react/event.html b/react/event.html index 1f76be35..834748c9 100644 --- a/react/event.html +++ b/react/event.html @@ -15,7 +15,7 @@
Skip to content
On this page

React 事件机制

React 事件机制

jsx
<div onClick={this.handleClick.bind(this)}>点我</div>
 
<div onClick={this.handleClick.bind(this)}>点我</div>
 

React 并不是将 click 事件绑定到了 div 的真实 DOM 上,而是在 document 处监听了所有的事件,当事件发生并且冒泡到 document 处的时候,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。

除此之外,冒泡到 document 上的事件也不是原生的浏览器事件,而是由 react 自己实现的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的话应该调用 event.preventDefault()方法,而不是调用 event.stopProppagation()方法。

JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document 上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。

另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault

实现合成事件的目的如下:

  • 合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力;
  • 对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。

React 的事件和普通的 HTML 事件有什么不同?

区别:

  • 对于事件名称命名方式,原生事件为全小写,react 事件采用小驼峰;
  • 对于事件函数处理语法,原生事件为字符串,react 事件为函数;
  • react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用preventDefault()来阻止默认行为。

合成事件是 react 模拟原生 DOM 事件所有能力的一个事件对象,其优点如下:

  • 兼容所有浏览器,更好的跨平台;
  • 将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。
  • 方便 react 统一管理和事务机制。

事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到 document 上合成事件才会执行。

React 组件中怎么做事件代理?它的原理是什么?

React 基于 Virtual DOM 实现了一个 SyntheticEvent 层(合成事件层),定义的事件处理器会接收到一个合成事件对象的实例,它符合 W3C 标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制,所有的事件都自动绑定在最外层上。

在 React 底层,主要对合成事件做了两件事:

  • 事件委派: React 会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数。
  • 自动绑定: React 组件中,每个方法的上下文都会指向该组件的实例,即自动绑定 this 为当前组件。
- + diff --git a/react/hooks.html b/react/hooks.html index a8186bec..a03c95cd 100644 --- a/react/hooks.html +++ b/react/hooks.html @@ -303,7 +303,7 @@

这里,首先假定 ExampleComponent 可见,然后再改变它的状态,让它不可见 。映射为真实的 DOM 操作是这样的,React 会创建一个 div 节点。

javascript
<div class="visible">visbile</div>
 
<div class="visible">visbile</div>
 

当把 visbile 的值变为 false 时,就会替换 class 属性为 hidden,并重写内部的 innerText 为 hidden。这样一个生成补丁、更新差异的过程统称为 diff 算法。

diff 算法可以总结为三个策略,分别从树、组件及元素三个层面进行复杂度的优化:

策略一:忽略节点跨层级操作场景,提升比对效率。(基于树进行对比)

这一策略需要进行树比对,即对树进行分层比较。树比对的处理手法是非常“暴力”的,即两棵树只对同一层次的节点进行比较,如果发现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用于进一步的比较,这就提升了比对效率。

策略二:如果组件的 class 一致,则默认为相似的树结构,否则默认为不同的树结构。(基于组件进行对比)

在组件比对的过程中:

只要父组件类型不同,就会被重新渲染。这也就是为什么 shouldComponentUpdate、PureComponent 及 React.memo 可以提高性能的原因。

策略三:同一层级的子节点,可以通过标记 key 的方式进行列表对比。(基于节点进行对比)

元素比对主要发生在同层级中,通过标记节点操作生成补丁。节点操作包含了插入、移动、删除等。其中节点重新排序同时涉及插入、移动、删除三个操作,所以效率消耗最大,此时策略三起到了至关重要的作用。通过标记 key 的方式,React 可以直接移动 DOM 节点,降低内耗。

React key

Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。

在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系。

注意事项:

虚拟 DOM 的引入与直接操作原生 DOM 相比,哪一个效率更高,为什么

虚拟 DOM 相对原生的 DOM 不一定是效率更高,如果只修改一个按钮的文案,那么虚拟 DOM 的操作无论如何都不可能比真实的 DOM 操作更快。在首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,虚拟 DOM 也会比 innerHTML 插入慢。它能保证性能下限,在真实 DOM 操作的时候进行针对性的优化时,还是更快的。所以要根据具体的场景进行探讨。

在整个 DOM 操作的演化过程中,其实主要矛盾并不在于性能,而在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM 不是别的,正是前端开发们为了追求更好的研发体验和研发效率而创造出来的高阶产物。虚拟 DOM 并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能。

React 与 Vue 的 diff 算法

diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁。

React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型节点的比对,分别是树、组件及元素,以此提升效率。

以上是经典的 React diff 算法内容。自 React 16 起,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNode 与 FiberTree 进行重构。fiberNode 使用了双链表的结构,可以直接找到兄弟节点与子节点。整个更新过程由 current 与 workInProgress 两株树双缓冲完成。workInProgress 更新完成后,再通过修改 current 相关指针指向新节点。

Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力,但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用防抖和节流去提高响应性能。

- + diff --git a/react/index.html b/react/index.html index b4d26e94..5777c6cd 100644 --- a/react/index.html +++ b/react/index.html @@ -929,7 +929,7 @@ ); }

错误边界

默认情况下,若一个组件在渲染期间(render)发生错误,会导致整个组件树全部被卸载

错误边界:是一个组件,该组件会捕获到渲染期间(render)子组件发生的错误,并有能力阻止错误继续传播

让某个组件捕获错误

  1. 编写生命周期函数 getDerivedStateFromError
    1. 静态函数
    2. 运行时间点:渲染子组件的过程中,发生错误之后,在更新页面之前
    3. 注意:只有子组件发生错误,才会运行该函数。自己发生错误处理不了
    4. 该函数返回一个对象,React 会将该对象的属性覆盖掉当前组件的 state
    5. 参数:错误对象
    6. 通常,该函数用于改变状态
  2. 编写生命周期函数 componentDidCatch
    1. 实例方法
    2. 运行时间点:渲染子组件的过程中,发生错误,更新页面之后,由于其运行时间点比较靠后,因此不太会在该函数中改变状态
    3. 通常,该函数用于记录错误消息

细节

某些错误,错误边界组件无法捕获

  1. 自身的错误
  2. 异步的错误
  3. 事件中的错误

这些错误,需要用 try catch 处理

总结:仅处理渲染子组件期间的同步错误

React 中的事件

这里的事件:React 内置的 DOM 组件中的事件

  1. 给 document 注册事件
  2. 几乎所有的元素的事件处理,均在 document 的事件中处理
    1. 一些不冒泡的事件,是直接在元素上监听
    2. 一些 document 上面没有的事件,直接在元素上监听
  3. 在 document 的事件处理,React 会根据虚拟 DOM 树的完成事件函数的调用
  4. React 的事件参数,并非真实的 DOM 事件参数,是 React 合成的一个对象,该对象类似于真实 DOM 的事件参数
    1. stopPropagation,阻止事件在虚拟 DOM 树中冒泡
    2. nativeEvent,可以得到真实的 DOM 事件对象
    3. 为了提高执行效率,React 使用事件对象池来处理事件对象

注意事项

  1. 如果给真实的 DOM 注册事件,阻止了事件冒泡,则会导致 react 的相应事件无法触发
  2. 如果给真实的 DOM 注册事件,事件会先于 React 事件运行
  3. 通过 React 的事件中阻止事件冒泡,无法阻止真实的 DOM 事件冒泡
  4. 可以通过 nativeEvent.stopImmediatePropagation(),阻止 document 上剩余事件的执行
  5. 在事件处理程序中,不要异步的使用事件对象,如果一定要使用,需要调用 persist 函数
- + diff --git a/react/lifecycle.html b/react/lifecycle.html index 9709514b..ab6b47b2 100644 --- a/react/lifecycle.html +++ b/react/lifecycle.html @@ -279,7 +279,7 @@ }

state 和 props 触发更新的生命周期分别有什么区别?

state 更新流程:

这个过程当中涉及的函数:

  1. shouldComponentUpdate: 当组件的 state 或 props 发生改变时,都会首先触发这个生命周期函数。它会接收两个参数:nextProps, nextState——它们分别代表传入的新 props 和新的 state 值。拿到这两个值之后,我们就可以通过一些对比逻辑来决定是否有 re-render(重渲染)的必要了。如果该函数的返回值为 false,则生命周期终止,反之继续;

注意:此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。应该考虑使用内置的 PureComponent 组件,而不是手动编写 shouldComponentUpdate()

  1. componentWillUpdate:当组件的 state 或 props 发生改变时,会在渲染之前调用 componentWillUpdate。componentWillUpdate 是 React16 废弃的三个生命周期之一。过去,我们可能希望能在这个阶段去收集一些必要的信息(比如更新前的 DOM 信息等等),现在我们完全可以在 React16 的 getSnapshotBeforeUpdate 中去做这些事;
  2. componentDidUpdate:componentDidUpdate() 会在 UI 更新后会被立即调用。它接收 prevProps(上一次的 props 值)作为入参,也就是说在此处我们仍然可以进行 props 值对比(再次说明 componentWillUpdate 确实鸡肋哈)。

props 更新流程:

相对于 state 更新,props 更新后唯一的区别是增加了对 componentWillReceiveProps 的调用。关于 componentWillReceiveProps,需要知道这些事情:

React 中发起网络请求应该在哪个生命周期中进行?为什么?

对于异步请求,最好放在 componentDidMount 中去操作,对于同步的状态改变,可以放在 componentWillMount 中,一般用的比较少。

如果认为在 componentWillMount 里发起请求能提早获得结果,这种想法其实是错误的,通常 componentWillMount 比 componentDidMount 早不了多少微秒,网络上任何一点延迟,这一点差异都可忽略不计。

react 的生命周期: constructor() -> componentWillMount() -> render() -> componentDidMount()

上面这些方法的调用是有次序的,由上而下依次调用。

总结:

React 16 中新生命周期有哪些

关于 React16 开始应用的新生命周期:

可以看出,React16 自上而下地对生命周期做了另一种维度的解读:

与此同时,新的生命周期在流程方面,仍然遵循“挂载”、“更新”、“卸载”这三个广义的划分方式。它们分别对应到:

- + diff --git a/react/react-interview.html b/react/react-interview.html index 7e42b2bb..4cbc9db7 100644 --- a/react/react-interview.html +++ b/react/react-interview.html @@ -1443,7 +1443,7 @@ name: PropTypes.string, };

当然,如果项目汇中使用了 TypeScript,那么就可以不用 PropTypes 来校验,而使用 TypeScript 定义接口来校验 props。

- + diff --git a/react/react-redux-router.html b/react/react-redux-router.html index bd4f5201..89ac8bbd 100644 --- a/react/react-redux-router.html +++ b/react/react-redux-router.html @@ -13,7 +13,7 @@
Skip to content
On this page

react-redux

  • React: 组件化的 UI 界面处理方案
  • React-Router: 根据地址匹配路由,最终渲染不同的组件
  • Redux:处理数据以及数据变化的方案(主要用于处理共享数据)

如果一个组件,仅用于渲染一个 UI 界面,而没有状态(通常是一个函数组件),该组件叫做展示组件 如果一个组件,仅用于提供数据,没有任何属于自己的 UI 界面,则该组件叫做容器组件,容器组件纯粹是为了给其他组件提供数据。

react-redux 库:链接 redux 和 react

  • Provider 组件:没有任何 UI 界面,该组件的作用,是将 redux 的仓库放到一个上下文中。
  • connect:高阶组件,用于链接仓库和组件的
    • 细节一:如果对返回的容器组件加上额外的属性,则这些属性会直接传递到展示组件
    • 第一个参数:mapStateToProps:
      • 参数 1:整个仓库的状态
      • 参数 2:使用者传递的属性对象
    • 第二个参数:
      • 情况 1:传递一个函数 mapDispatchToProps
        • 参数 1:dispatch 函数
        • 参数 2:使用者传递的属性对象
        • 函数返回的对象会作为属性传递到展示组件中(作为事件处理函数存在)
      • 情况 2:传递一个对象,对象的每个属性是一个 action 创建函数,当事件触发时,会自动的 dispatch 函数返回的 action
    • 细节二:如果不传递第二个参数,通过 connect 连接的组件,会自动得到一个属性:dispatch,使得组件有能力自行触发 action,但是,不推荐这样做。

知识

  1. chrome 插件:redux-devtools
  2. 使用 npm 安装第三方库:redux-devtools-extension

redux 和 router 的结合(connected-react-router)

希望把路由信息放进仓库统一管理的时候才需要这个库

用于将 redux 和 react-router 进行结合

本质上,router 中的某些数据可能会跟数据仓库中的数据进行联动

该组件会将下面的路由数据和仓库保持同步

  1. action:它不是 redux 的 action,它表示当前路由跳转的方式(PUSH、POP、REPLACE)
  2. location:它记录了当前的地址信息

该库中的内容:

connectRouter

这是一个函数,调用它,会返回一个用于管理仓库中路由信息的 reducer,该函数需要传递一个参数,参数是一个 history 对象。该对象,可以使用第三方库 history 得到。

routerMiddleware

该函数会返回一个 redux 中间件,用于拦截一些特殊的 action

ConnectedRouter

这是一个组件,用于向上下文提供一个 history 对象和其他的路由信息(与 react-router 提供的信息一致)

之所以需要新制作一个组件,是因为该库必须保证整个过程使用的是同一个 history 对象

一些 action 创建函数

  • push
  • replace
- + diff --git a/react/render.html b/react/render.html index 34d3787c..ab0f5ae0 100644 --- a/react/render.html +++ b/react/render.html @@ -241,7 +241,7 @@ } }

面试题 3:div 包住两个 App,再问执行顺序

队列:Comp1 App Comp1 App 左边执行完在执行右边而已(递归)

为什么不能写对象

对象可以构成 React 元素,但是没法构建节点,节点需要渲染,{a:1,b:2} 没法渲染

更新节点

更新的场景:

  1. 重新调用 ReactDOM.render,触发根节点更新
  2. 在类组件的实例对象中调用 setState,会导致该实例所在的节点更新

节点的更新

以上两点的后续步骤:

  1. 更新虚拟 DOM 树
  2. 完成真实的 DOM 更新
  3. 依次调用执行队列中的 componentDidMount
  4. 依次调用执行队列中的 getSnapshotBeforeUpdate
  5. 依次调用执行队列中的 componentDidUpdate

对比更新

将新产生的节点,对比之前虚拟 DOM 中的节点,发现差异,完成更新

问题:对比之前 DOM 树中哪个节点

React 为了提高对比效率,做出以下假设

  1. 假设节点不会出现层次的移动(对比时,直接找到旧树中对应位置的节点进行对比)
  2. 不同的节点类型会生成不同的结构
    1. 相同的节点类型:节点本身类型相同,如果是由 React 元素生成,type 值还必须一致
    2. 其他的,都属于不相同的节点类型
  3. 多个兄弟通过唯一标识(key)来确定对比的新节点

key 值的作用:用于通过旧节点,寻找对应的新节点,如果某个旧节点有 key 值,则其更新时,会寻找相同层级中的相同 key 值的节点,进行对比。

key 值应该在一个范围内唯一(兄弟节点中),并且应该保持稳定

找到了对比的目标

判断节点类型是否一致

根据不同的节点类型,做不同的事情

空节点:不做任何事情

DOM 节点

  1. 直接重用之前的真实 DOM 对象
  2. 将其属性的变化记录下来,以待将来统一完成更新(现在不会真正的变化)
  3. 遍历该新的 React 元素的子元素,递归对比更新

文本节点

  1. 直接重用之前的真实 DOM 对象
  2. 将新的文本变化记录下来,将来统一完成更新

组件节点

函数组件:重新调用函数,得到一个节点对象,进入递归对比更新

类组件

  1. 重用之前的实例
  2. 调用生命周期方法 getDerivedStateFromProps
  3. 调用生命周期方法 shouldComponentUpdate,若该方法返回 false,终止
  4. 运行 render,得到新的节点对象,进入递归对比更新
  5. 将该对象的 getSnapshotBeforeUpdate 加入队列
  6. 将该对象的 componentDidUpdate 加入队列

数组节点:遍历数组进行递归对比更新

整体上,卸载旧的节点,全新创建新的节点

创建新节点

进入新节点的挂载流程

卸载旧节点

  1. 文本节点、DOM 节点、数组节点、空节点、函数组件节点:直接放弃该节点,如果节点有子节点,递归卸载节点
  2. 类组件节点
    1. 直接放弃该节点
    2. 调用该节点的 componentWillUnMount 函数
    3. 递归卸载子节点

没有找到对比的目标

新的 DOM 树中有节点被删除

新的 DOM 树中有节点添加

- + diff --git a/react/transition.html b/react/transition.html index 225805ca..d2c0670c 100644 --- a/react/transition.html +++ b/react/transition.html @@ -13,7 +13,7 @@
Skip to content
On this page

React 动画

React 动画库:react-transition-group 文档在 npm 搜索https://reactcommunity.org/react-transition-group/

React 动画 - CSSTransition

当进入时,发生:

  1. 为 CSSTransition 内部的 DOM 根元素(后续统一称之为 DOM 元素)添加样式 enter
  2. 在一下帧(enter 样式已经完全应用到了元素),立即为该元素添加样式 enter-active
  3. 当 timeout 结束后,去掉之前的样式,添加样式 enter-done

当退出时,发生:

  1. 为 CSSTransition 内部的 DOM 根元素(后续统一称之为 DOM 元素)添加样式 exit
  2. 在一下帧(exit 样式已经完全应用到了元素),立即为该元素添加样式 exit-active
  3. 当 timeout 结束后,去掉之前的样式,添加样式 exit-done

设置classNames属性,可以指定类样式的名称

  1. 字符串:为类样式添加前缀
  2. 对象:为每个类样式指定具体的名称(非前缀)

关于首次渲染时的类样式,appear、apear-active、apear-done,它和 enter 的唯一区别在于完成时,会同时加入 apear-done 和 enter-done

还可以与 Animate.css 联用

React 动画 - SwitchTransition

和 CSSTransition 的区别:用于有秩序的切换内部组件

默认情况下:out-in 先退出后进入

  1. 当 key 值改变时,会将之前的 DOM 根元素添加退出样式(exit,exit-active)
  2. 退出完成后,将该 DOM 元素移除
  3. 重新渲染内部 DOM 元素
  4. 为新渲染的 DOM 根元素添加进入样式(enter, enter-active, enter-done)

in-out:

  1. 重新渲染内部 DOM 元素,保留之前的元素
  2. 为新渲染的 DOM 根元素添加进入样式(enter, enter-active, enter-done)
  3. 将之前的 DOM 根元素添加退出样式(exit,exit-active)
  4. 退出完成后,将该 DOM 元素移除

该库寻找 dom 元素的方式,是使用已经过时的 API:findDomNode,该方法可以找到某个组件下的 DOM 根元素,先保留,创建新的之后在删除

React 动画 - TransitionGroup

该组件的 children,接收多个 Transition 或 CSSTransition 组件,该组件用于根据这些子组件的 key 值,控制他们的进入和退出状态

- + diff --git a/react/umi.html b/react/umi.html index e6346bb8..0d4da4a0 100644 --- a/react/umi.html +++ b/react/umi.html @@ -13,7 +13,7 @@
Skip to content
On this page

umijs 简介

官网:https://umijs.org/

umijs, nextjs(ssr 服务端渲染),antd,antd-pro(antd+umijs)

  • 插件化
  • 开箱即用
  • 约定式路由

全局安装 umi

提供了一个命令行工具:umi,通过该命令可以对 umi 工程进行操作

umi 还可以使用对应的脚手架

  • dev: 使用开发模式启动工程
  • build:打包

约定式路由

umi 对路由的处理,主要通过两种方式:

  1. 约定式:使用约定好的文件夹和文件,来代表页面,umi 会根据开发者书写的页面,生成路由配置。
  2. 配置式:直接书写路由配置文件

路由匹配

  • umi 约定,工程中的 pages 文件夹中存放的是页面。如果工程包含 src 目录,则 src/pages 是页面文件夹。

  • umi 约定,页面的文件名,以及页面的文件路径,是该页面匹配的路由

  • umi 约定,如果页面的文件名是 index(不写 index 才能访问,写了反而不能访问了),则可以省略文件名(首页)(注意避免文件名和当前目录中的文件夹名称相同)

  • umi 约定,如果 src/layout 目录存在,则该目录中的 index.js 表示的是全局的通用布局,布局中的 children 则会添加具体的页面。

  • umi 约定,如果 pages 文件夹中包含_layout.js,则 layout.js 所在的目录以及其所有的子目录中的页面,共用该布局。

  • 404 约定,umi 约定,pages/404.js,表示 404 页面,如果路由无匹配,则会渲染该页面。该约定在开发模式中无效,只有部署后生效。

  • 使用$名称,会产生动态路由

路由跳转

  • 跳转链接: 导入umi/linkumi/navlink
  • 代码跳转: 导入umi/router

导入模块时,@表示 src 目录

路由信息的获取

所有的页面、布局组件,都会通过属性 props,收到下面的属性

  • match:等同于 react-router 的 match
  • history:等同于 react-router 的 history(history.location.query 被封装成了一个对象,使用的是 query-string 库进行的封装)
  • location:等同于 react-router 的 location(location.query 被封装成了一个对象,使用的是 query-string 库进行的封装)
  • route:对应的是路由配置

如果需要在普通组件中获取路由信息,则需要使用 withRouter 封装,可以通过umi/withRouter导入

配置式路由

当使用了路由配置后,约定式路由全部失效。

两种方式书写 umi 配置:

  1. 使用根目录下的文件.umirc.js
  2. 使用根目录下的文件config/config.js

进行路由配置时,每个配置就是一个匹配规则,并且,每个配置是一个对象,对象中的某些属性,会直接形成 Route 组件的属性

注意:

  • component 配置项,需要填写页面组件的路径,路径相对于 pages 文件夹
  • 如果配置项没有 exact,则会自动添加 exact 为 true
  • 每一个路由配置,可以添加任何属性
  • Routes 属性是一个数组,数组的每一项是一个组件路径,路径相对于项目根目录,当匹配到路由后,会转而渲染该属性指定的组件,并会将 component 组件作为 children 放到匹配的组件中

路由配置中的信息,同样可以放到约定式路由中,方式是,为约定式路由添加第一个文档注释(注释的格式的 YAML),需要将注释放到最开始的位置

YAML 格式

  • 键值对,冒号后需要加上空格
  • 如果某个属性有多个键或多个值,需要进行缩进(空格,不能 tab)

使用 dva

官方插件集 umi-plugin-react 文档:https://umijs.org/zh/plugin/umi-plugin-react.html

dva 插件和 umi 整合后,将模型分为两种:

  1. 全局模型:所有页面通用,工程一开始启动后,模型就会挂载到仓库
  2. 局部模型:只能被某些页面使用,访问具体的页面时才会挂载到仓库

定义全局模型

src/models目录下定义的 js 文件都会被看作是全局模型,默认情况下,模型的命名空间和文件名一致。

定义局部模型

局部模型定义在 pages 文件夹或其子文件夹中,在哪个文件夹定义的模型,会被该文件夹中的所有页面以及子页面、以及该文件夹的祖先文件夹中的页面所共享。

局部模型的定义和全局模型的约定类似,需要创建一个 models 文件夹

使用样式

解决两个问题:

  1. 保证类样式名称的唯一性:css-module
  2. 样式代码的重复:less 或 sass

局部样式和全局样式

底层使用了 webpack 的加载器:css-loader(内部包含了 css-module 的功能)

css 文件 -> css-module -> 对象

  1. 某个组件特有的样式,不与其他组件共享,通常,将该样式文件与组件放置在同一个目录(非强制性)(要保证类样式名称唯一)
  2. 如果某些样式可能被某些组件共享,这样的样式,通常放到 assets/css 文件夹中。(要保证类样式名称唯一)
  3. 全局样式,名称一定唯一,不需要 css-module 处理。umijs 约定,src/global.css 样式,是全局样式,不会交给 css-module 处理。

less

less 代码 -> less-loader -> css 代码 -> css-module -> 对象

代理和数据模拟

代理

代理用于解决跨域问题

配置.umirc.js中的 proxy,配置方式和 devServer 中的 proxy 配置相同

数据模拟

用于解决前后端协同开发的问题

数据模拟可以让前端开发者在开发时,无视后端接口是否真正完成,因为使用的是模拟的数据

umijs 约定:

  1. mock 文件夹中的文件
  2. src/pages 文件夹中的_mock.js 文件

以上两种 JS 文件,均会被 umijs 读取,并作为数据模拟的配置

可以自行发挥,添加模拟数据,通常,我们会和 mockjs 配合。

配置

额外的约定文件

  • src/pages/document.ejs: 页面模板文件
  • src/global.js:在 umi 最开始启动时运行的 js 文件
  • src/app.js:作运行时配置的代码
    • patchRoutes: 函数,该函数会在 umi 读取完所有静态路由配置后执行
    • dva
      • config: 相当于 new dva(配置)
      • plugins: 相当于 dva.use(插件)
  • .env: 配置环境变量,这些变量会在 umi 编译期间发挥作用
    • UMI_ENV:umi 的环境变量值,可以是任意值,该值会影响到.umirc.js
    • PORT
    • MOCK

umirc 配置

umi 配置

书写在.umirc.js 文件中的配置

  • plugins:配置 umijs 的插件
  • routes:配置路由(会导致约定式路由失效)
  • history:history 对象模式(默认是 browser)
  • outputPath:使用 umi build 后,打包的目录名称,默认./dist
  • base: 相当于之前 BrowserRouter 中的 basename
  • publicPath: 指定静态资源所在的目录
  • exportStatic: 开启该配置后,会打包成多个静态页面,每个页面对应一个路由,开启多静态页面应用的前提条件是:没有动态路由

webpack 配置

umi 脚手架

create-umi

- + diff --git a/react/utils.html b/react/utils.html index 726a66dc..57c591a0 100644 --- a/react/utils.html +++ b/react/utils.html @@ -13,7 +13,7 @@
Skip to content
On this page

工具

严格模式

StrictMode(React.StrictMode),本质是一个组件,该组件不进行 UI 渲染(React.Fragment <> </>),它的作用是,在渲染内部组件时,发现不合适的代码。

  • 识别不安全的生命周期
  • 关于使用过时字符串 ref API 的警告
  • 关于使用废弃的 findDOMNode 方法的警告
  • 检测意外的副作用
    • React 要求,副作用代码仅出现在以下生命周期函数中
    • ComponentDidMount
    • ComponentDidUpdate
    • ComponentWillUnMount

副作用:一个函数中,做了一些会影响函数外部数据的事情,例如:

  1. 异步处理
  2. 改变参数值
  3. setState
  4. 本地存储
  5. 改变函数外部的变量

相反的,如果一个函数没有副作用,则可以认为该函数是一个纯函数

在严格模式下,虽然不能监控到具体的副作用代码,但它会将不能具有副作用的函数调用两遍,以便发现问题。(这种情况,仅在开发模式下有效)

  • 检测过时的 context API

Profiler

性能分析工具

分析某一次或多次提交(更新),涉及到的组件的渲染时间

火焰图:得到某一次提交,每个组件总的渲染时间以及自身的渲染时间

排序图:得到某一次提交,每个组件自身渲染时间的排序

组件图:某一个组件,在多次提交中,自身渲染花费的时间

- + diff --git a/ts/TypeScript-onePage.html b/ts/TypeScript-onePage.html index 88bfa44b..ccae2309 100644 --- a/ts/TypeScript-onePage.html +++ b/ts/TypeScript-onePage.html @@ -2207,7 +2207,7 @@

声明文件

概述、编写、发布

概述

  1. 什么是声明文件?

.d.ts结尾的文件

  1. 声明文件有什么作用?

为 JS 代码提供类型声明

  1. 声明文件的位置

编写声明文件

手动编写   自动生成

工程是使用 ts 开发的,发布(编译)之后,是 js 文件,发布的是 js 文件。

如果发布的文件,需要其他开发者使用,可以使用声明文件,来描述发布结果中的类型。

配置tsconfig.json中的declaration:true即可

  1. 对已有库,它是使用 js 书写而成,并且更改该库的代码为 ts 成本较高,可以手动编写声明文件
  2. 对一些第三方库,它们使用 js 书写而成,并且这些第三方库没有提供声明文件,可以手动编写声明文件。

全局声明

声明一些全局的对象、属性、变量

namespace: 表示命名空间,可以将其认为是一个对象,命名空间中的内容,必须通过命名空间.成员名访问

模块声明

三斜线指令

在一个声明文件中,包含另一个声明文件

发布

  1. 当前工程使用 ts 开发

编译完成后,将编译结果所在文件夹直接发布到 npm 上即可

  1. 为其他第三方库开发的声明文件

发布到@types/**中。

1) 进入 github 的开源项目:https://github.com/DefinitelyTyped/DefinitelyTyped

2) fork 到自己的开源库中

3) 从自己的开源库中克隆到本地

4) 本地新建分支(例如:mylodash4.3),在新分支中进行声明文件的开发

在types目录中新建文件夹,在新的文件夹中开发声明文件
 
在types目录中新建文件夹,在新的文件夹中开发声明文件
 

5) push 分支到你的开源库

6) 到官方的开源库中,提交 pull request

7) 等待官方管理员审核(1 天)

审核通过之后,会将你的分支代码合并到主分支,然后发布到 npm。

之后,就可以通过命令npm install @types/你发布的库名

- + diff --git a/vue/SSR.html b/vue/SSR.html index 39721dc5..ced95850 100644 --- a/vue/SSR.html +++ b/vue/SSR.html @@ -13,7 +13,7 @@
Skip to content
On this page

SSR

SPA

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 基于上面一点,SPA 相对对服务器压力小;
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

Vue SSR 的实现原理

  • app.js 作为客户端与服务端的公用入口,导出 Vue 根实例,供客户端 entry 与服务端 entry 使用。客户端 entry 主要作用挂载到 DOM 上,服务端 entry 除了创建和返回实例,还需要进行路由匹配与数据预获取。
  • webpack 为客服端打包一个 ClientBundle,为服务端打包一个 ServerBundle
  • 服务器接收请求时,会根据 url,加载相应组件,获取和解析异步数据,创建一个读取 Server BundleBundleRenderer,然后生成 html 发送给客户端。
  • 客户端混合,客户端收到从服务端传来的 DOM 与自己的生成的 DOM 进行对比,把不相同的 DOM 激活,使其可以能够响应后续变化,这个过程称为客户端激活(也就是转换为单页应用)。为确保混合成功,客户 端与服务器端需要共享同一套数据。在服务端,可以在渲染之前获取数据,填充到 store 里,这样,在客户端挂载到 DOM 之前,可以直接从 store 里取数据。首屏的动态数据通过 window.INITIAL_STATE 发送到客户端
  • VueSSR 的原理,主要就是通过 vue-server-rendererVue 的组件输出成一个完整 HTML,输出到客户端,到达客户端后重新展开为一个单页应用。
- + diff --git a/vue/challages.html b/vue/challages.html index dead7e6a..2dbc6370 100644 --- a/vue/challages.html +++ b/vue/challages.html @@ -495,7 +495,7 @@ // 设置没有的属性,删除属性监控不到,所以要用$set,$delete - + diff --git a/vue/component-communication.html b/vue/component-communication.html index 394dbce9..7d6ad26a 100644 --- a/vue/component-communication.html +++ b/vue/component-communication.html @@ -399,7 +399,7 @@ } }

缺点:无法跟踪数据的变化,如果组件数变得复杂,任何组件都有权改动他,仓库数据出了问题,难以判断那个步骤出现问题。

eventbus

组件通知事件总线发生了某件事,事件总线通知其他监听该事件的所有组件运行某个函数

$emit$listeners通信的异同

相同点:均可实现子组件向父组件传递消息

差异点:

- + diff --git a/vue/computed.html b/vue/computed.html index e3566ecc..ff28c43e 100644 --- a/vue/computed.html +++ b/vue/computed.html @@ -13,7 +13,7 @@
Skip to content
On this page

computed

Vue2 中 computed 源码解读

methods

vue 对 methods 的处理比较简单,只需要遍历 methods 配置中的每个属性,将其对应的函数使用 bind 绑定当前组件实例后复制其引用到组件实例中即可

computed

当组件实例触发生命周期函数beforeCreate后,它会做一系列事情,其中就包括对 computed 的处理

它会遍历 computed 配置中的所有属性,为每一个属性创建一个 Watcher 对象,并传入一个函数,该函数的本质其实就是 computed 配置中的 getter,这样一来,getter 运行过程中就会收集依赖

但是和渲染函数不同,为计算属性创建的 Watcher 不会立即执行,因为要考虑到该计算属性是否会被渲染函数使用,如果没有使用,就不会得到执行。因此,在创建 Watcher 的时候,它使用了 lazy 配置,lazy 配置可以让 Watcher 不会立即执行。

收到lazy的影响,Watcher 内部会保存两个关键属性来实现缓存,一个是value,一个是dirty

value属性用于保存 Watcher 运行的结果,受lazy的影响,该值在最开始是undefined

dirty属性用于指示当前的value是否已经过时了,即是否为脏值,受lazy的影响,该值在最开始是true

Watcher创建好后,vue 会使用代理模式,将计算属性挂载到组件实例中

当读取计算属性时,vue 检查其对应的 Watcher 是否是脏值,如果是,则运行函数,计算依赖,并得到对应的值,保存在 Watcher 的 value 中,然后设置 dirty 为 false,然后返回。

如果 dirty 为 false,则直接返回 watcher 的 value,即为缓存的原理 巧妙的是,在依赖收集时,被依赖的数据不仅会收集到计算属性的 Watcher,还会收集到组件的 Watcher

当计算属性的依赖变化时,会先触发计算属性的 Watcher执行,此时,它只需设置**dirty**为 true 即可,不做任何处理。

由于依赖同时会收集到组件的 Watcher,因此组件会重新渲染,而重新渲染时又读取到了计算属性,由于计算属性目前已为 dirty,因此会重新运行 getter 进行运算

而对于计算属性的 setter,则极其简单,当设置计算属性时,直接运行 setter 即可

Vue3 中 computed 源码解读

TODO

常见问题

面试官:computed 和 methods 有什么区别?

我:

  1. 在使用时,computed 当做属性使用,而 methods 则当做方法调用
  2. computed 可以具有 getter 和 setter,因此可以赋值,而 methods 不行
  3. computed 无法接收多个参数,而 methods 可以
  4. computed 具有缓存,而 methods 没有

面试官:回去等通知吧!

更深入的回答:↑

watch 与 computed 的区别是什么?

  1. 都是观察数据变化的(相同)
  2. 计算属性将会混入到 vue 的实例中,所以需要监听自定义变量;watch 监听 data 、props 里面数据的变化;
  3. computed 有缓存,它依赖的值变了才会重新计算,watch 没有;
  4. watch 支持异步,computed 不支持;
  5. watch 是一对多(监听某一个值变化,执行对应操作);computed 是多对一(监听属性依赖于其他属性)
  6. watch 监听函数接收两个参数,第一个是最新值,第二个是输入之前的值;
  7. computed 属性是函数时,都有 get 和 set 方法,默认走 get 方法,get 必须有返回值(return)

watch 的 参数:deep:深度监听;immediate :组件加载立即触发回调函数执行

对于 Computed:

  • 它支持缓存,只有依赖的数据发生了变化,才会重新计算
  • 不支持异步,当 Computed 中有异步操作时,无法监听数据的变化
  • computed 的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于 data 声明过,或者父组件传递过来的 props 中的数据进行计算的。
  • 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用 computed
  • 如果 computed 属性的属性值是函数,那么默认使用 get 方法,函数的返回值就是属性的属性值;在 computed 中,属性有一个 get 方法和一个 set 方法,当数据发生变化时,会调用 set 方法。

对于 Watch:

  • 它不支持缓存,数据变化时,它就会触发相应的操作
  • 支持异步监听
  • 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
  • 当一个属性发生变化时,就需要执行相应的操作
  • 监听数据必须是 data 中声明的或者父组件传递过来的 props 中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
    • immediate:组件加载立即触发回调函数
    • deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep 无法监听到数组和对象内部的变化。

当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用 watch。 总结:

  • computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
  • watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

运用场景:

  • 当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时都要重新计算。
  • 当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

Vue 中要获取当前时间你会放到 computed 还是 methods 里?

放在 computed 里面。因为 computed 只有在它的相关依赖发生改变时才会重新求值。相比而言,方法只要发生重新渲染,methods 调用总会执行所有函数。

- + diff --git a/vue/diff.html b/vue/diff.html index 4163a8c3..b9f1a359 100644 --- a/vue/diff.html +++ b/vue/diff.html @@ -205,7 +205,7 @@ }; </script>

解决:只要不是索引即可,比如,直接使用 item。这样,key 就是永远不变的,更新前后都是一样的,并且又由于节点的内容本来就没变,所以 Diff 算法完美生效,只需将新节点添加到真实 DOM 就行了。

总结

当组件创建和更新时,vue 均会执行内部的 update 函数,该函数使用 render 函数生成的虚拟 dom 树,将新旧两树进行对比,找到差异点,最终更新到真实 dom

对比差异的过程叫 diff,vue 在内部通过一个叫 patch 的函数完成该过程

在对比时,vue 采用深度优先、同层比较的方式进行比对。

在判断两个节点是否相同时,vue 是通过虚拟节点的 key 和 tag来进行判断的

具体来说

这样一直递归的遍历下去,直到整棵树完成对比。

注意:

random 是生成随机数,有一定概率多个 item 会生成相同的值,不能保证唯一。

如果是根据数据来生成 item,数据具有 id 属性,那么就可以使用 id 来作为 key

如果不是根据数据生成 item,那么最好的方式就是使用时间戳来作为 key。或者使用诸如 uuid 之类的库来生成唯一的 id

使用 index 作为 key 和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列, 导致 Vue 会复用错误的旧子节点,做很多额外的工作。

对比 Vue3

简单来说,diff 算法有以下过程

正常 Diff 两个树的时间复杂度是 O(n^3),但实际情况下我们很少会进行跨层级的移动 DOM,所以 VueDiff 进行了优化,从O(n^3) -> O(n),只有当新旧 children 都为多个子节点时才需要用核心的 Diff 算法进行同层级比较。

Vue2 的核心 Diff 算法采用了双端比较的算法,同时从新旧 children 的两端开始进行比较,借助 key 值找到可复用的节点,再进行相关操作。相比 ReactDiff 算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。

Vue3.x 借鉴了 ivi 算法和 inferno 算法

在创建 VNode 时就确定其类型,以及在 mount/patch 的过程中采用位运算来判断一个 VNode 的类型,在这个基础之上再配合核心的 Diff 算法,使得性能上较 Vue2.x 有了提升。该算法中还运用了动态规划的思想求解最长递归子序列。

- + diff --git a/vue/directive.html b/vue/directive.html index d62eb560..1bc0396e 100644 --- a/vue/directive.html +++ b/vue/directive.html @@ -13,7 +13,7 @@
Skip to content
On this page

Vue 指令篇

描述下 Vue 自定义指令

在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。 一般需要对 DOM 元素进行底层操作时使用,尽量只用来操作 DOM 展示,不修改内部的值。当使用自定义指令直接修改 value 值时绑定 v-model 的值也不会同步更新;如必须修改可以在自定义指令中使用 keydown 事件,在 vue 组件中使用 change 事件,回调中修改 vue 数据;

(1)自定义指令基本内容

  • 全局定义:Vue.directive("focus",{})

  • 局部定义:directives:{focus:

  • 钩子函数:指令定义对象提供钩子函数

    • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
    • inSerted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。
    • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新。
    • ComponentUpdate:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
    • unbind:只调用一次,指令与元素解绑时调用。
  • 钩子函数参数

    • el:绑定元素
    • bing: 指令核心对象,描述指令全部信息属性
    • name
    • value
    • oldValue
    • expression
    • arg
    • modifers
    • vnode   虚拟节点
    • oldVnode:上一个虚拟节点(更新钩子函数中才有用)

(2)使用场景

  • 普通 DOM 元素进行底层操作的时候,可以使用自定义指令
  • 自定义指令是用来操作 DOM 的。尽管 Vue 推崇数据驱动视图的理念,但并非所有情况都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的 DOM 操作,并且是可复用的。

(3)使用案例 初级应用:

  • 鼠标聚焦
  • 下拉菜单
  • 相对时间转换
  • 滚动动画

高级应用:

  • 自定义指令实现图片懒加载
  • 自定义指令集成第三方插件
- + diff --git a/vue/interviewer.html b/vue/interviewer.html index b0975ddf..2536c1e8 100644 --- a/vue/interviewer.html +++ b/vue/interviewer.html @@ -13,7 +13,7 @@
Skip to content
On this page

今天我是面试官

vue 中 key 的功能是什么?

  • 用于虚拟节点树之间,diff 时更好地比较 如果不设置 key,或者用 index 作为 key 会有什么问题?

为什么用 vuex,不用 event bus? 这两者有什么区别?

vue router 的实现原理

  • Location   href, path
  • 知道多页面应用中,如何区分前端路由和后端路由?
  • 路由哈希模式和 history 模式的区别 -
  • 把 nextTick 中的回调放到事件循环的微任务队列中。
  1. vue 发送请求在 created 生命周期钩子中,尽量避免 DOM 渲染挂载后再发送请求,更新数据,更改 DOM;
  2. vue router 组件懒加载;
  3. index.html 文件控制在 14kb 之内,这是基于 TCP 的慢开始规则,第一个响应包的大小就是 14kb,应该包含浏览器开始渲染页面所需的所有内容,或者至少包含页面模板(第一次渲染所需的 CSS 和 HTML)

以头条为例,采用组件化方式设计一个信息流?

  • 组件功能抽象能力
  • 组件设计思路
  • 功能划分:多种文章显示格式,刷新提示,评论/媒体/时间等信息显示,refresh/loadmore 功能,dislike 功能,动态广告
  • 卡片形式抽象:单图、多图、无图、视频、ugc、刷新条等

要求:有清晰思路,良好的信息流组件设计模式,可扩展性强并给出核心功能的实现方式

请简述什么是双向数据绑定和单向数据流,以及它们的区别

双向数据绑定

双向数据绑定意味着 UI 动态地绑定到模型数据,这样当 UI 改变时,模型数据就随之变化,反之亦然。

单向数据流

单向数据流意味着 model 是唯一来源。UI 触发消息的变化,将用户行为标记为 model。只有 model 具有访问更改应用程序状态的权限。其效果是数据总是朝一个方向流动,这使得理解起来更容易。

二者有什么优缺点

单向数据流是确定性的,数据流动方向可以跟踪,流动单一,追查问题的时候可以跟快捷。缺点就是写起来不太方便。要使 UI 发生变更就必须创建各种 action 来维护对应的 state 双向绑定,优点是使用方便,值和 UI 双绑定,但是由于各种数据相互依赖相互绑定,导致数据问题的源头难以被跟踪到,子组件修改父组件,兄弟组件互相修改有有违设计原则

可以介绍一下模板引擎的原理,比如实现类似 html 这种模板将其中变量替换为对应值的方式。

介绍下你所理解的 MVVM 框架,如 Angular、React、Vue 都解决了什么问题?

要求:能够从每个框架的生态系统,甚至结合之前的项目及不同的业务特点,给出框架的优劣

Vue 框架中组件消息通信方式

父子之间层级过多时,当父子组件之间层级不多的时候,父组件可以一层层的向子组件传递数据或者子组件一层层向父组件发送消息,代码上没有太难维护的地方。可是,一旦父子组件之间层级变多后,传递一个数据或者发送一个消息就变得麻烦。这块如果了解开源的 Element 组件库,就会知道其实现方式:构造一个函数自动向上/向下查询父亲节点,以[组件名, 消息名, 参数]三元组进行消息传递,降低长链传播成本;

具体实现参考:https://github.com/ElemeFE/element/blob/dev/src/mixins/emitter.js

如何理解虚拟 DOM?

对虚拟 dom 和 diff 算法中的一些细节理解

https://github.com/livoras/blog/issues/13

要求:写出 diff 算法的核心部分

- + diff --git a/vue/keep-alive-LRU.html b/vue/keep-alive-LRU.html index 48d5d9d6..c9982df9 100644 --- a/vue/keep-alive-LRU.html +++ b/vue/keep-alive-LRU.html @@ -23,7 +23,7 @@ this.keys = [] }

keep-alive 中的生命周期哪些

keep-alive 是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染 DOM。

如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。

当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated 钩子函数。

LRU 缓存算法

TODO

- + diff --git a/vue/lifecycle.html b/vue/lifecycle.html index 217cf5ef..1de51c83 100644 --- a/vue/lifecycle.html +++ b/vue/lifecycle.html @@ -65,7 +65,7 @@ new Vue(vnode.componentOptions);
  1. 运行生命周期钩子函数created
  2. 渲染:生成render函数:如果有配置,直接使用配置的render,如果没有,使用运行时编译器,把模板编译为render
  3. 运行生命周期钩子函数beforeMount
  4. 创建一个Watcher,传入一个函数updateComponent,该函数会运行render,把得到的vnode再传入_update函数执行。 在执行render函数的过程中,会收集所有依赖,将来依赖变化时会重新运行updateComponent函数 在执行_update函数的过程中,触发patch函数,由于目前没有旧树,因此直接为当前的虚拟 dom 树的每一个普通节点生成 elm 属性,即真实 dom。 如果遇到创建一个组件的 vnode,则会进入组件实例化流程,该流程和创建 vue 实例流程基本相同,递归,最终会把创建好的组件实例挂载 vnode 的componentInstance属性中,以便复用。
  5. 运行生命周期钩子函数mounted

Vue 父子组件挂载顺序

重渲染?

  1. 数据变化后,所有依赖该数据的Watcher均会重新运行,这里仅考虑updateComponent函数对应的Watcher
  2. Watcher会被调度器放到nextTick中运行,也就是微队列中,这样是为了避免多个依赖的数据同时改变后被多次执行
  3. 运行生命周期钩子函数beforeUpdate
  4. updateComponent函数重新执行
  1. 运行生命周期钩子函数updated

Vue 父子组件重新渲染顺序

总体流程

它可以总共分为 8 个阶段:创建前/后, 载入前/后,更新前/后,销毁前/销毁后。

注意:

常见问题

接口请求一般放在哪个生命周期中?

接口请求可以放在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

但是推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

Vue 子组件和父组件执行顺序

加载渲染过程:

  1. 父组件 beforeCreate
  2. 父组件 created
  3. 父组件 beforeMount
  4. 子组件 beforeCreate
  5. 子组件 created
  6. 子组件 beforeMount
  7. 子组件 mounted
  8. 父组件 mounted

更新过程:

  1. 父组件 beforeUpdate
  2. 子组件 beforeUpdate
  3. 子组件 updated
  4. 父组件 updated

销毁过程:

  1. 父组件 beforeDestroy
  2. 子组件 beforeDestroy
  3. 子组件 destroyed
  4. 父组件 destoryed
- + diff --git a/vue/nextTick.html b/vue/nextTick.html index a730e9b2..8527151f 100644 --- a/vue/nextTick.html +++ b/vue/nextTick.html @@ -15,7 +15,7 @@
Skip to content
On this page

$nextTick 工作原理

DANGER

写作中

Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。

作用:vue 更新 DOM 是异步更新的,数据变化,DOM 的更新不会马上完成,nextTick 的回调是在下次 DOM 更新循环结束之后执行的延迟回调。

nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout 的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。

nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理

nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例,引入异步更新队列机制的原因 ∶

  • 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,可以减少一些无用渲染
  • 同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用 VirtualDOM 进行计算得出需要更新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要

Vue 采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作 DOM。有时候,可能遇到这样的情况,DOM1 的数据发生了变化,而 DOM2 需要从 DOM1 中获取数据,那这时就会发现 DOM2 的视图并没有更新,这时就需要用到了 nextTick 了。

由于 Vue 的 DOM 操作是异步的,所以,在上面的情况中,就要将 DOM2 获取数据的操作写在$nextTick 中。

vue
this.$nextTick(() => { // 获取数据的操作...})
 
this.$nextTick(() => { // 获取数据的操作...})
 

所以,在以下情况下,会用到 nextTick:

  • 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的 DOM 结构的时候,这个操作就需要方法在 nextTick()的回调函数中。
  • 在 vue 生命周期中,如果在 created()钩子进行 DOM 操作,也一定要放在 nextTick()的回调函数中。

因为在 created()钩子函数中,页面的 DOM 还未渲染,这时候也没办法操作 DOM,所以,此时如果想要操作 DOM,必须将操作的代码放在 nextTick()的回调函数中。 nextTick:可以做什么不可以做什么? nextTick:里面调用 update 会是什么情况? 如果我循环更新 dom 节点并且执行它,会有什么结果? 循环调用的话 nextTick:里面有容错机制吗?

实现原理:nextTick 主要使用了宏任务和微任务。根据执行环境分别尝试采用

  • Promise:可以将函数延迟到当前函数调用栈最末端
  • MutationObserver :是 H5 新加的一个功能,其功能是监听 DOM 节点的变动,在所有 DOM 变动完成后,执行回调函数
  • setImmediate:用于中断长时间运行的操作,并在浏览器完成其他操作(如事件和显示更新)后立即运行回调函数
  • 如果以上都不行则采用 setTimeout 把函数延迟到 DOM 更新之后再使用

原因是宏任务消耗大于微任务,优先使用微任务,最后使用消耗最大的宏任务。

参考资料

https://juejin.cn/post/6844903557372575752

- + diff --git a/vue/reactive.html b/vue/reactive.html index 98931041..a0e0e852 100644 --- a/vue/reactive.html +++ b/vue/reactive.html @@ -717,7 +717,7 @@ } } - + diff --git a/vue/slot.html b/vue/slot.html index a6542af1..41c326ff 100644 --- a/vue/slot.html +++ b/vue/slot.html @@ -13,7 +13,7 @@
Skip to content
On this page

一篇精通 slot

DANGER

写作中

slot 是什么?有什么作用?

slot 又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用 slot 元素作为承载分发内容的出口。插槽 slot 是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot 又分三类,默认插槽,具名插槽和作用域插槽。

  • 默认插槽:又名匿名查抄,当 slot 没有指定 name 属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
  • 具名插槽:带有具体名字的插槽,也就是带有 name 属性的 slot,一个组件可以出现多个具名插槽。
  • 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。本质是子组件可以通过插槽的位置绑定一些数据,让父组件插槽位置可以用这个数据。

实现原理

当子组件 vm 实例化时,获取到父组件传入的 slot 标签的内容,存放在 vm.slot 中,默认插槽为 um. slot.default,具名插槽为 vm.slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到 slot 标签,使用 slot 中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

- + diff --git a/vue/v-model.html b/vue/v-model.html index 0db56e6b..0065818c 100644 --- a/vue/v-model.html +++ b/vue/v-model.html @@ -41,7 +41,7 @@ <!-- 等效于 --> <Comp :number="data" @change="data=$event" />

总结

首先要对数据进行劫持监听,所以我们需要设置一个监听器 Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者 Watcher 看是否需要更新。

因为订阅者是有很多个,所以我们需要有一个消息订阅器 Dep 来专门收集这些订阅者,然后在监听器 Observer 和订阅者 Watcher 之间进行统一管理的。

接着,我们还需要有一个指令解析器 Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者 Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者 Watcher 接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。

因此接下去我们执行以下 3 个步骤,实现数据的双向绑定:

  1. 实现一个监听器 Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。

  2. 实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。

  3. 实现一个解析器 Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。

- + diff --git a/vue/vdom.html b/vue/vdom.html index f4e7b511..b43d0295 100644 --- a/vue/vdom.html +++ b/vue/vdom.html @@ -67,7 +67,7 @@

这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化

深度遍历 AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的 DOM 永远不会改变,这对运行时模板更新起到了极大的优化作用。

(3)生成代码

javascript
const code = generate(ast, options);
 
const code = generate(ast, options);
 

generate 将 ast 抽象语法树编译成 render 字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function(render) 生成 render 函数。

- + diff --git a/vue/vs.html b/vue/vs.html index 8d52c719..d1c6ece5 100644 --- a/vue/vs.html +++ b/vue/vs.html @@ -13,7 +13,7 @@
Skip to content
On this page

Vue 和 React 的核心区别

VueAngular 以及 React 的区别是什么?

关于 Vue 和其他框架的不同,官方专门写了一篇文档,从性能、体积、灵活性等多个方面来进行了说明。 详细可以参阅:https://cn.vuejs.org/v2/guide/comparison.html

Composition API 与 React Hook 很像,区别是什么

从 React Hook 的实现角度看,React Hook 是根据 useState 调用的顺序来确定下一次重渲染时的 state 是来源于哪个 useState,所以出现了以下限制

  • 不能在循环、条件、嵌套函数中调用 Hook
  • 必须确保总是在你的 React 函数的顶层调用 Hook
  • useEffect、useMemo 等函数必须手动确定依赖关系

而 Composition API 是基于 Vue 的响应式系统实现的,与 React Hook 的相比

  • 声明在 setup 函数内,一次组件实例化只调用一次 setup,而 React Hook 每次重渲染都需要调用 Hook,使得 React 的 GC 比 Vue 更有压力,性能也相对于 Vue 来说也较慢
  • Compositon API 的调用不需要顾虑调用顺序,也可以在循环、条件、嵌套函数中使用
  • 响应式系统自动实现了依赖收集,进而组件的部分的性能优化由 Vue 内部自己完成,而 React Hook 需要手动传入依赖,而且必须必须保证依赖的顺序,让 useEffect、useMemo 等函数正确的捕获依赖变量,否则会由于依赖不正确使得组件性能下降。

虽然 Compositon API 看起来比 React Hook 好用,但是其设计思想也是借鉴 React Hook 的。

Vue 和 React 区别

定位相同:处理 UI 层的,vue 提倡渐进式处理,react 没有 vue 推崇模版写法,react 是 all in js jsx, vue 支持 jsx, react 不支持 vue 的 template react hooks, vue3 借鉴了,都有 hoos 风格的 api UI 更新策略:react 传入一个新数据,不能修改旧数据,vue 会根据两次数据渲染 dom 的 diff 更新 UI,数据变化,就会计划更新 UI,都会延迟更新 vue 文化:全部封装好;React 推崇第三方库结合 vue 有 keep-alive, react 没有,重新渲染,需要自己实现 vue css scoped;react 需要第三方:css modules/style-component

相似之处:

  • 都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库;
  • 都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板;
  • 都使用了 Virtual DOM(虚拟 DOM)提高重绘性能;
  • 都有 props 的概念,允许组件间的数据传递;
  • 都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用性。

不同之处 :

1)数据流 Vue 默认支持数据双向绑定,而 React 一直提倡单向数据流 2)虚拟 DOM Vue2.x 开始引入"Virtual DOM",消除了和 React 在这方面的差异,但是在具体的细节还是有各自的特点。

  • Vue 宣称可以更快地计算出 Virtual DOM 的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
  • 对于 React 而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过 PureComponent/shouldComponentUpdate 这个生命周期方法来进行控制,但 Vue 将此视为默认的优化。

3)组件化 React 与 Vue 最大的不同是模板的编写。

  • Vue 鼓励写近似常规 HTML 的模板。写起来很接近标准 HTML 元素,只是多了一些属性。
  • React 推荐你所有的模板通用 JavaScript 的语法扩展——JSX 书写。

具体来讲:React 中 render 函数是支持闭包特性的,所以 import 的组件在 render 中可以直接调用。但是在 Vue 中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 一个组件完了之后,还需要在 components 中再声明下。 4)监听数据变化的实现原理不同

  • Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能
  • React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的 vDOM 的重新渲染。这是因为 Vue 使用的是可变数据,而 React 更强调数据的不可变。

5)高阶组件 react 可以通过高阶组件(HOC)来扩展,而 Vue 需要通过 mixins 来扩展。 高阶组件就是高阶函数,而 React 的组件本身就是纯粹的函数,所以高阶函数对 React 来说易如反掌。相反 Vue.js 使用 HTML 模板创建视图组件,这时模板无法有效的编译,因此 Vue 不能采用 HOC 来实现。 6)构建工具 两者都有自己的构建工具:

  • React ==> Create React APP
  • Vue ==> vue-cli

7)跨平台

  • React ==> React Native
  • Vue ==> Weex

Vue 的优点

  • 轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb ;
  • 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
  • 双向数据绑定:保留了 angular 的特点,在数据操作方面更为简单;
  • 组件化:保留了 react 的优点,实现了 html 的封装和重用,在构建单页面应用方面有着独特的优势;
  • 视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
  • 虚拟 DOM:dom 操作是非常耗费性能的,不再使用原生的 dom 操作节点,极大解放 dom 操作,但具体操作的还是 dom 不过是换了另一种方式;
  • 运行速度更快:相比较于 react 而言,同样是操作虚拟 dom,就性能而言, vue 存在很大的优势。
- + diff --git a/vue/vue-cli.html b/vue/vue-cli.html index e841eef7..4fb93c77 100644 --- a/vue/vue-cli.html +++ b/vue/vue-cli.html @@ -13,7 +13,7 @@
Skip to content
On this page

vue-cli 到底帮我们做了什么

vue-cli 中的工程化

  1. vue.js:vue-cli 工程的核心,主要特点是双向数据绑定和组件系统。
  2. vue-router:vue 官方推荐使用的路由框架。
  3. vuex:专为 Vue.js 应用项目开发的状态管理器,主要用于维护 vue 组件间共用的一些 变量 和 方法。
  4. axios(或者 fetch、ajax):用于发起 GET 、或 POST 等 http 请求,基于 Promise 设计。
  5. vux 等:一个专为 vue 设计的移动端 UI 组件库。
  6. webpack:模块加载和 vue-cli 工程打包器。
  7. eslint:代码规范工具

vue-cli 工程常用的 npm 命令有哪些?

下载 node_modules 资源包的命令:npm install

启动 vue-cli 开发环境的 npm 命令:npm run dev

vue-cli 生成 生产环境部署资源 的 npm 命令:npm run build

用于查看 vue-cli 生产环境部署资源文件大小的 npm 命令:npm run build --report

- + diff --git a/vue/vue-compile.html b/vue/vue-compile.html index 5ebb4f28..c6a2c8a1 100644 --- a/vue/vue-compile.html +++ b/vue/vue-compile.html @@ -13,7 +13,7 @@
Skip to content
On this page

Vue 编译器为什么如此强大

说一下 vue 模版编译的原理是什么

简单说,Vue 的编译过程就是将 template 转化为 render 函数的过程。会经历以下阶段:

  • 生成 AST
  • 优化
  • codegen

首先解析模版,生成 AST 语法树(一种用 JavaScript 对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。

Vue 的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的 DOM 也不会变化。那么优化过程就是深度遍历 AST 树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。

编译的最后一步是将优化后的 AST 树转换为可执行的代码。

说一下 Vue complier 的实现原理是什么样的?

在使用 vue 的时候,我们有两种方式来创建我们的 HTML 页面,第一种情况,也是大多情况下,我们会使用模板 template 的方式,因为这更易读易懂也是官方推荐的方法;第二种情况是使用 render 函数来生成 HTML,它比 template 更接近最终结果。

complier 的主要作用是解析模板,生成渲染模板的 render, 而 render 的作用主要是为了生成 VNode

complier 主要分为 3 大块:

  • parse:接受 template 原始模板,按着模板的节点和数据生成对应的 ast
  • optimize:遍历 ast 的每一个节点,标记静态节点,这样就知道哪部分不会变化,于是在页面需要更新时,通过 diff 减少去对比这部分 DOM,提升性能
  • generate 把前两步生成完善的 ast,组成 render 字符串,然后将 render 字符串通过 new Function 的方式转换成渲染函数

Vue 模版编译原理

vue 中的模板 template 无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML 语法,所有需要将 template 转化成一个 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML 元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。模板编译又分三个阶段,解析 parse,优化 optimize,生成 generate,最终生成可执行函数 render。

  • 解析阶段:使用大量的正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为抽象语法树 AST。
  • 优化阶段:遍历 AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,优化 runtime 的性能。
  • 生成阶段:将最终的 AST 转化为 render 函数字符串。

源码实现

TODO

- + diff --git a/vue/vue-interview.html b/vue/vue-interview.html index f58a1a8f..9c070886 100644 --- a/vue/vue-interview.html +++ b/vue/vue-interview.html @@ -319,7 +319,7 @@
Vue.mixin({
   beforeCreate() {        // ...逻辑        // 这种方式会影响到每个组件的 beforeCreate 钩子函数    }})
 

虽然文档不建议在应用中直接使用 mixin,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax 或者一些工具函数等等。

mixins 应该是最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins 混入代码,比如上拉下拉加载数据这种逻辑等等。 另外需要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。

内置组件 Transition

官网详细文档:https://cn.vuejs.org/v2/guide/transitions.html

时机

Transition组件会监控slot唯一根元素的出现和消失,并会在其出现和消失时应用过渡效果 Transition不生成任何元素,只是为了生成过渡效果 具体的监听内容是:

流程

类名规则:

  1. 如果transition上没有定义name,则类名为v-xxxx
  2. 如果transition上定义了name,则类名为${name}-xxxx
  3. 如果指定了类名,直接使用指定的类名

指定类名见:自定义过渡类名

1. 进入效果

2. 消失效果

过渡组

Transision可以监控其内部的单个 dom 元素的出现和消失,并为其附加样式

如果要监控一个 dom 列表,就需要使用TransitionGroup组件

它会对列表的新增元素应用进入效果,删除元素应用消失效果,对被移动的元素应用v-move样式

被移动的元素之所以能够实现过渡效果,是因为TransisionGroup内部使用了 Flip 过渡方案

- + diff --git a/vue/vue-router.html b/vue/vue-router.html index cbdc1b71..f2d40e7d 100644 --- a/vue/vue-router.html +++ b/vue/vue-router.html @@ -305,7 +305,7 @@ }

二、Vue 路由钩子在生命周期函数的体现

  1. 完整的路由导航解析流程(不包括其他生命周期)
  1. 触发钩子的完整顺序

路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从 a 组件离开,第一次进入 b 组件 ∶

  1. 导航行为被触发到导航完成的整个过程

Vue-router 跳转和 location.href 有什么区别

params 和 query 的区别

用法:query 要用 path 来引入,params 要用 name 来引入,接收参数都是类似的,分别是 this.$route.query.namethis.$route.params.name

url 地址显示:query 更加类似于 ajax 中 get 传参,params 则类似于 post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示

注意:query 刷新不会丢失 query 里面的数据 params 刷新会丢失 params 里面的数据。

Vue-router 导航守卫有哪些

- + diff --git a/vue/vue3-onepage.html b/vue/vue3-onepage.html index a55b59d1..cd5873ea 100644 --- a/vue/vue3-onepage.html +++ b/vue/vue3-onepage.html @@ -783,7 +783,7 @@ }; </script>

说一下 vue3.0 是如何变得更快的?

优化 Diff 算法

相比 Vue 2Vue 3 采用了更加优化的渲染策略。去掉不必要的虚拟 DOM 树遍历和属性比较,因为这在更新期间往往会产生最大的性能开销。

这里有三个主要的优化:

在没有动态改变节点结构的模板指令(例如 v-ifv-for)的情况下,节点结构保持完全静态。

当更新节点时,不再需要递归遍历 DOM 树。所有的动态绑定部分将在一个平面数组中跟踪。这种优化通过将需要执行的树遍历量减少一个数量级来规避虚拟 DOM 的大部分开销。

编译器还根据需要执行的更新类型,为每个具有动态绑定的元素生成一个优化标志。

例如,具有动态类绑定和许多静态属性的元素将收到一个标志,提示只需要进行类检查。运行时将获取这些提示并采用专用的快速路径。

综合起来,这些技术大大改进了渲染更新基准,Vue 3.0 有时占用的 CPU 时间不到 Vue 2 的十分之一。

体积变小

重写后的 Vue 支持了 tree-shaking,像修剪树叶一样把不需要的东西给修剪掉,使 Vue 3.0 的体积更小。

需要的模块才会打入到包里,优化后的 Vue 3.0 的打包体积只有原来的一半(13kb)。哪怕把所有的功能都引入进来也只有 23kb,依然比 Vue 2.x 更小。像 keep-alive、transition 甚至 v-for 等功能都可以按需引入。

并且 Vue 3.0 优化了打包方法,使得打包后的 bundle 的体积也更小。

官方所给出的一份惊艳的数据:打包大小减少 41%,初次渲染快 55%,更新快 133%,内存使用减少 54%

说一说相比 vue3.x 对比 vue2.x 变化

  1. 源码组织方式变化:使用 TS 重写
  2. 支持 Composition API:基于函数的 API,更加灵活组织组件逻辑(vue2 用的是 options api)
  3. 响应式系统提升:Vue3 中响应式数据原理改成 proxy,可监听动态新增删除属性,以及数组变化
  4. 编译优化:vue2 通过标记静态根节点优化 diff,Vue3 标记和提升所有静态根节点,diff 的时候只需要对比动态节点内容
  5. 打包体积优化:移除了一些不常用的 api(inline-template、filter)
  6. 生命周期的变化:使用 setup 代替了之前的 beforeCreate 和 created
  7. Vue3 的 template 模板支持多个根标签
  8. Vuex 状态管理:创建实例的方式改变,Vue2 为 new Store , Vue3 为 createStore
  9. Route 获取页面实例与路由信息:vue2 通过 this 获取 router 实例,vue3 通过使用 getCurrentInstance/ userRoute 和 userRouter 方法获取当前组件实例
  10. Props 的使用变化:vue2 通过 this 获取 props 里面的内容,vue3 直接通过 props
  11. 父子组件传值:vue3 在向父组件传回数据时,如使用的自定义名称,如 backData,则需要在 emits 中定义一下
- + diff --git a/vue/vuex.html b/vue/vuex.html index 13c3499a..421d19b2 100644 --- a/vue/vuex.html +++ b/vue/vuex.html @@ -73,7 +73,7 @@ })

如何在组件中批量使用 Vuex

使用 mapGetters 辅助函数, 利用对象展开运算符将 getter 混入 computed 对象中 使用 mapMutations 辅助函数,在组件中这么使用

- +