暂时未有相关云产品技术能力~
慢慢认识世界,慢慢更新自己。大家好,我是柒八九。由于,新公司的项目打包是用的Vite,而之前的所参与的项目都是用Webpack作为打包工具,原来对Vite的了解,只是一个把玩工具,没有过多的深入了解。本着干一行,爱一行的职业态度。所以,就找了很多相关资料学习和研究。以下的内容,都是基于本人对Vite的个人见解。不一定对,随便看看你能所学到的知识点vite 是个啥? 推荐阅读指数 ⭐️⭐️⭐️vite 打包阶段 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️打包阶段的插件执行顺序 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️Vite+React的项目打包优化(简单版) 推荐阅读指数 ⭐️⭐️⭐️⭐️好了,天不早了,干点正事哇。这里再多絮叨几句,下面的大部分内容,都是从Vite打包阶段聊,针对HMR一些内容,没有涉及。vite 是个啥?Vite是一种现代化的前端构建工具,它的目标是提供一种快速、简单和易于使用的开发体验。Vite使用了一些新的技术来实现快速的开发体验,这些技术包括ES模块、即时编译和热重载。ES模块是一种新的JavaScript模块格式,它是浏览器原生支持的。ES模块提供了一种简单、可读、可扩展的方式来组织代码,同时还提供了静态分析和优化的机会。Vite利用ES模块的特性,将应用程序拆分成更小的代码块,使得应用程序的加载时间更快。如果想了解更多的关于ES模块的相关概念,可以参考之前的文章。你真的了解ESM吗?Vite使用即时编译来实现更快的开发体验。即时编译是指在代码更改时,Vite会立即编译代码并将其发送到浏览器中。这意味着开发人员不需要等待编译过程完成,就可以看到更改后的效果。这大大缩短了开发周期,并提高了开发效率。最后,Vite还使用热重载技术。热重载是指在开发过程中,如果更改了代码,应用程序将自动重新加载,而无需手动刷新页面。这使得开发人员能够更快地看到更改后的效果,并且不会丢失任何数据。想了解更多关于Vite的介绍,可以参考官网vite 打包阶段在讨论Vite时,通常关注的是其作为开发服务器以及它如何在开发过程中实现即时反馈。但是,在生产环境下,Vite也会大放异彩。下面,我们就一起来了解一下Vite是如何在生产环境下,对你的项目代码进行打包/优化处理的。本文假定你已经对Vite有一定的了解。如果第一次听说,你可以先移步到为什么选 Vite来了解相关的设计理念和解决哪些痛点。用上帝视角看Vite如何处理资源在一个 Vite 项目中,index.html 在项目最外层而不是在 public 文件夹内。这是有意而为之的:在本地开发阶段 Vite 是一个资源服务器,而 index.html 是该 Vite 项目的入口文件。Vite 将 index.html 视为源码和模块图的一部分。这个HTML文件用于引入网站资源信息。通过设置type="module"的script标签引入JS资源(外部资源和内联脚本)通过设置 rel="stylesheet"的link引入外部样式<!DOCTYPE html> <html> <head> <script type="module" src="/src/main.ts"></script> <script type="module"> // 内联 script </script> <link rel="stylesheet" href="/src/main.css"> <style> internal-style: {} </style> </head> <body> <div id="app"></div> </body> </html> 复制代码Vite接收HTML文件,来查找每个加载的JS模块、内联脚本和CSS样式表。JS源代码通过Rollup处理,解析和转换内部依赖项,生成一个不经常更新的vendor.js(带有依赖项)和一个index.js(应用程序的其余部分)。这些源文件被转换成的带有 hash 值的文件,以实现强缓存。任何在JS中被引用的CSS文件都会被打包到index.css文件中内部样式不会被处理。script或import可以指向任何文件类型,只要Vite知道如何转译它们即可。在上面的情况下,main.ts文件需要在打包过程中转换为JS文件。在Vite中,使用基于Go的打包工具esbuild实现对应资源的转换。在资源被转换后,也会生成一个新的资源地址,用它们替换原来的资源地址。并且Vite执行import的静态分析,为每个JS插入模块预加载标记(rel="modulepreload" ),使浏览器可以并行加载这些资源,从而避免加载瀑布效应。<!DOCTYPE html> <html> <head> <script type="module" src="/assets/index.d93758c6.js"></script> <link rel="modulepreload" href="/assets/vendor.a9c538d6.js"> <link rel="stylesheet" href="/assets/index.3015a40c.css"> <style> internal-style: {} </style> </head> <body> <div id="app"></div> </body> </html> 复制代码Vite针对JS和CSS资源支持代码分割。当遇到动态导入时,会生成一个异步JS块和一个CSS块。其他资源,如图像、视频、wasm,可以使用相对路径导入。在生成其输出文件时,Vite还会对这些文件进行hash处理,并重写JS和CSS文件中的URL并指向它们。另一方面,public文件夹中的资源会按原样复制到输出根目录,并允许用户通过绝对路径引用这些文件。每个JS和CSS块都需要在生产中进行压缩。自从Vite 2.6.0以来,Vite也使用esbuild为两种语言执行压缩任务,以加快构建过程。Rollup的二次封装Vite 应用的打包过程,是基于Rollup 的二次封装 。Vite中可以直接使用现有的Rollup插件,实现很多开箱即用的功能。但是,需要注意一些与 Rollup 插件的兼容性问题,但大多数来自 Rollup生态系统的插件都可以直接作为 Vite 插件工作。Vite内部打包流程下图展示了,Vite打包的大体流程。 上面的流程主要分四个步骤收集打包配置信息确认打包后的资源路径使用rollup打包处理资源输出当你执行vite build时,Vite CLI被运行。运行命令是用cac实现的。该操作触发了 build 函数。await build({ root, base, mode, config, logLevel, clearScreen, buildOptions }) 复制代码build又调用 doBuild。实现逻辑如下。async function doBuild(inlineConfig: InlineConfig = {}): RollupOutput{ // ①收集打包配置信息 const config = await resolveConfig(inlineConfig, 'build', 'production') // ②确认打包后的资源路径 const outDir = resolve(config.build.outDir) prepareOutDir(outDir, config) // ③打包处理 const bundle = await rollup.rollup({ input: resolve('index.html'), plugins: config.plugins }) // ④资源输出 return await bundle.write({ dir: outDir, format: 'es', exports: 'auto', sourcemap: config.build.sourcemap, entryFileNames: path.join(config.assetsDir, `[name].[hash].js`), chunkFileNames: path.join(config.assetsDir, `[name].[hash].js`), assetFileNames: path.join(config.assetsDir, `[name].[hash].[ext]`), manualChunks: createMoveToVendorChunkFn() }) } 复制代码首先,调用resolveConfig用于解析用户配置、项目配置文件和Vite默认值来生成一个具体的ResolvedConfig。const config = await resolveConfig(inlineConfig, 'build', 'production') 复制代码接下来,确认好输出目录,并在生成资产之前清空它。这个函数还将publicDir的内容复制到项目dist文件夹。const outDir = resolve(config.build.outDir) prepareOutDir(outDir, config) 复制代码然后,基于index.html和config.plugins创建了rollup的bundle对象。const bundle = await rollup.rollup({ input: resolve('index.html'), plugins: config.plugins }) 复制代码最后,bundle.write被调用,用于生成输出目录中的资源信息。return await bundle.write({ dir: outDir, format: 'es', exports: 'auto', sourcemap: config.build.sourcemap, entryFileNames: path.join(options.assetsDir, `[name].[hash].js`), chunkFileNames: path.join(options.assetsDir, `[name].[hash].js`), assetFileNames: path.join(options.assetsDir, `[name].[hash].[ext]`), manualChunks: createMoveToVendorChunkFn() }) 复制代码createMoveToVendorChunkFn函数定义了默认的分块策略,定义了JS被打包后,何种资源被分配到index.js和vendor.js中。具体实现如下:function createMoveToVendorChunkFn() { return (id, { getModuleInfo }) => { if ( id.includes('node_modules') && !isCSSRequest(id) && staticImportedByEntry(id, getModuleInfo) ) { return 'vendor' } } } 复制代码通过对Vite的打包做了一个简单的分析,我们可以得知:Vite的构建过程,就是以Rollup为基础,借助插件对资源的二次处理过程。常见的插件插件在开发阶段和打包阶段是通过resolvedPlugins进行解析。Vite在打包时通过resolveBuildPlugins插入额外的插件,以处理压缩和其他优化。有一些关键插件。vite:build-html和vite:html-inline-proxy-plugin用于处理HTML,将JS和CSS替换为经Vite优化过的对应资源。vite:css和vite:css-post用于处理CSS和预处理器。vite:esbuild用于为每个模块转换TypeScript和JSX。vite:asset用于管理静态资源。vite:build-import-analysis用于预加载优化、支持全局导入和URL重写。vite:esbuild-transpile用于将chunks转换为合适的目标和压缩对应资源。还有一些插件是官方Rollup插件aliascommonjsrollup-plugin-dynamic-import-variables打包阶段的插件执行顺序就webpack来说,plugins的作用在于强化其构建过程中,所遇到的一些工程化的问题,比如代码压缩,资源压缩等,所以vite作为新时代的构建工具,理应当也具有插件系统来解决构建项目的整个生命周期中所遇到的工程化的问题,说白了,插件就是为了解决某一类型的问题而出现的一个或一种工具函数。比如lodash,他被称之为一个库,也可以认作是一个插件。所以vite会在不同的生命周期中调用不同的插件去达成不同的目的。让我们深入了解每个插件的使用方式和职责权限。一个 Vite 插件可以额外指定一个 enforce 属性来调整它的应用顺序。enforce 的值可以是pre 或 post。解析后的插件将按照以下顺序排列:Alias带有 enforce: 'pre' 的用户插件(前置插件)Vite 核心插件没有 enforce 值的用户插件(常规插件)Vite 构建用的插件带有 enforce: 'post' 的用户插件(后置插件)Vite 后置构建插件(最小化,manifest,报告)Vite构建运行的插件的执行顺序如下:alias带有 enforce: 'pre' 的用户插件(前置插件)vite:modulePreloadvite:resolvevite:html-inline-proxy-pluginvite:cssvite:esbuildvite:jsonvite:wasmvite:workervite:asset没有 enforce 值的用户插件(常规插件)vite:definevite:css-postvite:build-htmlcommonjsvite:data-urirollup-plugin-dynamic-import-variablesvite:asset-import-meta-url带有 enforce: 'post' 的用户插件(后置插件)vite:build-import-analysisvite:esbuild-transpilevite:terservite:manifestvite:ssr-manifestvite:reporter1. alias该插件,用于在打包时定义别名,与Webpack中的resolve.alias相同。可以是一个对象Record或一个{ find, replacement, customResolver }的数组Array<{ find: string | RegExp, replacement: string, customResolver?: ResolverFunction | ResolverObject }>用以下方法配置Viteresolve: { alias: { '@components': path.resolve(__dirname, 'src/components') } } 复制代码可以让你从源码中的任何地方导入你想要导出的数据import Button from '@components/Button.tsx' 复制代码这个插件解析路径并将它们转译为真实的路径import Button from '../../components/Button.tsx' 复制代码2. 带有 enforce: 'pre' 的用户插件(前置插件)这些是带有enforce: 'pre'的插件。例如,@rollup/plugin-image配置了该属性,它就在Vite的内置插件之前运行。import image from "@rollup/plugin-image" export default { plugins: [ { ...image(), enforce: 'pre', }, ] } 复制代码3. vite:modulePreload类型: boolean | { polyfill?: boolean, resolveDependencies?: ResolveModulePreloadDependenciesFn }默认值: { polyfill: true }默认情况下,一个模块预加载 polyfill 会被自动注入。该 polyfill 会自动注入到每个 index.html 入口的的代理模块中。如果构建通过 build.rollupOptions.input 被配置为了使用非 HTML 入口的形式,那么必须要在你的自定义入口中手动引入该 polyfill:import 'vite/modulepreload-polyfill' 复制代码polyfill 实现<script> function processPreload () { const fetchOpts = {}; if (script.integrity) fetchOpts.integrity = script.integrity; if (script.referrerpolicy) fetchOpts.referrerPolicy = script.referrerpolicy; if (script.crossorigin === 'use-credentials') fetchOpts.credentials = 'include'; else if (script.crossorigin === 'anonymous') fetchOpts.credentials = 'omit'; else fetchOpts.credentials = 'same-origin'; fetch(link.href, fetchOpts) .then(res => res.ok && res.arrayBuffer()); } const links = document.querySelectorAll('link[rel=modulepreload]'); for (const link of links) processPreload(link); </script> 复制代码该polyfill允许Vite预加载模块以避免加载瀑布,支持非Chromium浏览器。4. vite:resolve它使用Node解析算法来定位node_modules中的第三方模块。它与官方的rollup插件不同,因为需要对Vite特定功能(SSR和devServer)进行特殊处理。Node 文件定位5. vite:html-inline-proxy-plugin该插件将入口HTML文件中的内联脚本作为单独的模块加载。这些脚本由vite:build-html插件将其从HTML中删除,并替换为一个type="module"的script。6. vite:css该插件与vite:css-post插件一起使用来实现Vite的CSS功能。支持预处理器(postCSS、sass、less),包括解析导入的URL。7. vite:esbuild类型: ESBuildOptions | falseESBuildOptions 继承自 esbuild 转换选项。最常见的用例是自定义 JSX:export default defineConfig({ esbuild: { jsxFactory: 'h', jsxFragment: 'Fragment', }, }) 复制代码默认情况下,esbuild 会被应用在 ts、jsx、tsx 文件。你可以通过 esbuild.include 和 esbuild.exclude 对要处理的文件类型进行配置。此外,你还可以通过 esbuild.jsxInject 来自动为每一个被 esbuild 转换的文件注入 JSX helper。export default defineConfig({ esbuild: { jsxInject: `import React from 'react'`, }, }) 复制代码8. vite:json处理JSON文件的导入。// 导入整个对象 import json from './example.json' // 导入一个根字段作为命名的出口, 便于tree shaking import { field } from './example.json' 复制代码9. vite:wasm此插件允许用户直接导入预编译的.wasm文件。预编译的 .wasm 文件可以通过 ?init 来导入。默认导出一个初始化函数,返回值为所导出 wasm 实例对象的 Promise:import init from './example.wasm?init' init().then((instance) => { instance.exports.test() }) 复制代码在生产构建当中,体积小于 assetInlineLimit 的 .wasm 文件将会被内联为 base64 字符串。否则,它们将作为资源复制到 dist 目录中,并按需获取。10. 'vite:worker'通过构造器导入一个 Web Worker 可以使用 new Worker() 和 new SharedWorker() 导入。与 worker 后缀相比,这种语法更接近于标准,是创建 worker 的 推荐 方式。const worker = new Worker( new URL('./worker.js', import.meta.url) ) 复制代码worker 构造函数会接受可以用来创建 “模块” worker 的选项:const worker = new Worker( new URL('./worker.js', import.meta.url), { type: 'module',} ) 复制代码带有查询后缀的导入你可以在导入请求上添加 ?worker 或 ?sharedworker 查询参数来直接导入一个 web worker 脚本。默认导出会是一个自定义 worker 的构造函数:import MyWorker from './worker?worker' const worker = new MyWorker() 复制代码默认情况下,worker 脚本将在生产构建中编译成单独的 chunk。如果你想将 worker 内联为 base64 字符串,请添加 inline 查询参数:import MyWorker from './worker?worker&inline' 复制代码如果你想要以一个 URL 的形式读取该 worker,请添加 url 这个 query:import MyWorker from './worker?worker&url' 复制代码11. 'vite:asset'该插件用于资源的处理。将资源引入为 URL引入一个静态资源会返回解析后的公共路径:import imgUrl from './img.png' document.getElementById('hero-img').src = imgUrl 复制代码例如,imgUrl 在开发时会是 /img.png,在生产构建后会是 /assets/img.2d8efhg.png。常见的图像、媒体和字体文件类型被自动检测为资源。你可以使用 assetsInclude 选项扩展内部列表。(当从 HTML 引用它们或直接通过 fetch 或 XHR 请求它们时,它们将被插件转换管道排除在外。)引用的资源作为构建资源图的一部分包括在内,将生成散列文件名,并可以由插件进行处理以进行优化。较小的资源体积小于 assetsInlineLimit 选项值 则会被内联为 base64 data URL。显式 URL 引入未被包含在内部列表或 assetsInclude 中的资源,可以使用 ?url 后缀显式导入为一个 URL。import workletURL from 'extra-scalloped-border/worklet.js?url' CSS.paintWorklet.addModule(workletURL) 复制代码将资源引入为字符串资源可以使用 ?raw 后缀声明作为字符串引入。import shaderString from './shader.glsl?raw' 复制代码public 目录如果你有下列这些资源:不会被源码引用(例如 robots.txt)必须保持原有文件名(没有经过 hash)...或者你压根不想引入该资源,只是想得到其 URL。那么你可以将该资源放在指定的 public 目录中,它应位于你的项目根目录。该目录中的资源在开发时能直接通过 / 根路径访问到,并且打包时会被完整复制到目标目录的根目录下。目录默认是 /public,但可以通过 publicDir 选项 来配置。请注意:引入public中的资源永远应该使用根绝对路径举个例子,public/icon.png 应该在源码中被引用为 /icon.png。public 中的资源不应该被 JavaScript 文件引用。12. 常规插件没有 enforce 值的用户插件13. 'vite:define'定义全局常量替换方式。其中每项在开发环境下会被定义在全局,而在构建时被静态替换。类型: RecordString 值会以原始表达式形式使用,所以如果定义了一个字符串常量,它需要被显式地打引号。(例如使用 JSON.stringify)define: { __APP_VERSION__: `JSON.stringify(${version})` } 复制代码对于使用 TypeScript 的项目,还需要 env.d.ts 或 vite-env.d.ts 文件中添加类型声明,以获得类型检查以及代码提示。// vite-env.d.ts declare const __APP_VERSION__: string 复制代码14. vite:css-post这个插件用esbuild对CSS资源进行最小化。css资源的URL占位符被解析为其最终的构建路径。它还实现了CSS代码拆分。Vite 会自动地将一个异步 chunk 模块中使用到的 CSS 代码抽取出来并为其生成一个单独的文件。这个 CSS 文件将在该异步 chunk 加载完成时自动通过一个 标签载入,该异步 chunk会保证只在 CSS 加载完毕后再执行,避免发生 FOUC 。{无样式内容的闪光|flash of unstyled content}(FOUC)是指在加载外部CSS样式表之前,网页以浏览器的默认样式短暂出现的情况,这是由于网络浏览器引擎在检索到所有信息之前渲染了该网页。下面是Vite中预加载插件的的简化版本。function createLink(dep) { // JS -> <link rel="modulepreload" href="dep" /> // CSS -> <link rel="stylesheet" href="dep" /> } function preload(importModule, deps) { return Promise.all( deps.map(dep => { if (!alreadyLoaded(dep)) { document.head.appendChild(createLink(dep)) if (isCss(dep)) { // 等CSS资源加载,避免出现FOUC return new Promise((resolve, reject) => { link.addEventListener('load', resolve) link.addEventListener('error', reject) }) } } }) ).then(() => importModule()) } 复制代码这个插件将使用上面的辅助函数来转换动态导入。以下是import('./async.js') 复制代码将被转换为preload( () => import('/assets/async.js), ['/assets/async.css','/assets/async-dep.js'] ) 复制代码如果build.cssCodeSplit的值为false,这些块会被vite:build-html插件作为注入。15. vite:build-html这个插件会将 HTML 文件中的 标签编译成一个 JS 模块。它会在 transform 钩子中移除 HTML 中的 script 标签,生成一个 JS文件,用于引入每个模块和资源文件。随后,在 generateBundle 钩子中插入 JS 文件,并使用 vite:asset 插件插入资源文件。16. commonjs它将CommonJS模块转换为ES6,这样它们就可以被包含在Rollup的包中。在开发过程中,Vite使用esbuild进行资源的pre-bundling,它负责将CommonJS转换为ES6但在构建过程中,没有pre-bundling的这步,所以需要commonjs插件。17. vite:data-uri它从data-URI导入模块。Data URL,即前缀为 data: 协议的 URL,其允许内容创建者向文档中嵌入小文件。Data URL 由四个部分组成:前缀(data:)、指示数据类型的 MIME 类型、如果非文本则为可选的 base64 标记、数据本身:data:[][;base64], 我们可以从DataURL中导入模块。import batman from 'data:application/json;base64, eyAiYmF0bWFuIjogInRydWUiIH0='; 复制代码18. rollup/plugin-dynamic-import-vars用于支持动态导入中的变量。它是用build.dynamicImportVarsOptions来配置的。import dynamicImportVars from '@rollup/plugin-dynamic-import-vars'; export default { plugins: [ dynamicImportVars({ // options }) ] }; 复制代码允许用户编写动态解析的导入:function importLocale(locale) { return import(`./locales/${locale}.js`); } 复制代码19. vite:asset-import-meta-url将 new URL(path, import.meta.url)转换为内置URLimport.meta.url 是一个 ESM 的原生功能,会暴露当前模块的 URL。将它与原生的 URL 构造器 组合使用,在一个 JavaScript 模块中,通过相对路径我们就能得到一个被完整解析的静态资源 URL:const imgUrl = new URL('./img.png', import.meta.url).href document.getElementById('hero-img').src = imgUrl 复制代码这个模式同样还可以通过字符串模板支持动态 URL:function getImageUrl(name) { return new URL(`./dir/${name}.png`, import.meta.url).href } 复制代码在生产构建时,Vite 才会进行必要的转换保证 URL 在打包和资源哈希后仍指向正确的地址。然而,这个 URL 字符串必须是静态的,这样才好分析。否则代码将被原样保留、因而在 build.target 不支持 import.meta.url时会导致运行时错误。// Vite 不会转换这个 const imgUrl = new URL(imagePath, import.meta.url).href 复制代码20. 后置插件带有 enforce: 'post' 的用户插件。21. vite:build-import-analysis这个插件会对 URL 导入进行词法分析、解析、重写和分析。动态导入会增加预加载指令。在客户端代码中注入一个辅助函数,用于在异步块本身异步加载时并行预加载 CSS 和直接导入的异步块。Glob 导入会被识别并使用 transformImportGlob 进行转译。例如:const modules = import.meta.glob('./dir/*.js') 复制代码被转化为const modules = { './dir/foo.js': () => import('./dir/foo.js'), './dir/bar.js': () => import('./dir/bar.js') } 复制代码22. vite:esbuild-transpile这个插件对每个渲染的块进行转译,以支持配置的目标。如果build.minify是 "esbuild"(Vite3+的版本是默认值),它也将使用esbuild来最小化代码,避免了对terser 的需求。它比 terser 快 20-40 倍,压缩率只差 1%-2%。23. vite:terser如果build.minify是'terser',这个插件就会被用来使用terser对每个渲染的块进行最小化。24. vite:manifest当设置为 true,构建后将会生成 manifest.json 文件,包含了没有被 hash 过的资源文件名和 hash 后版本的映射。可以为一些服务器框架渲染时提供正确的资源引入链接。当该值为一个字符串时,它将作为 manifest 文件的名字。25. vite:ssr-manifest当设置为 true 时,构建也将生成 SSR 的 manifest 文件,以确定生产中的样式链接与资产预加载指令。当该值为一个字符串时,它将作为 manifest 文件的名字。26. vite:reporter一个记录进度的插件,以及一份包含生成块和资源信息的报告。$ vite build vite v3.1.0 building for production... ✓ 34 modules transformed. dist/assets/favicon.17e50649.svg 1.49 KiB dist/assets/logo.ecc203fb.svg 2.61 KiB dist/index.html 0.52 KiB dist/assets/index.3015a40c.js 1.39 KiB / gzip: 0.73 KiB dist/assets/index.d93758c6.css 0.77 KiB / gzip: 0.49 KiB dist/assets/vendor.a9c538d6.js 129.47 KiB / gzip: 41.77 KiB Done in 2.90s. 复制代码Vite+React的项目打包优化(简单版)可以从以下几点出发代码分割预取/预加载代码压缩图片压缩缓存策略代码分割使用代码分割可以将代码划分成较小的块,从而减少页面加载时间。可以使用Vite提供的import()函数或React的React.lazy()函数来实现代码分割。import React, { lazy, Suspense } from 'react'; const LazyComponent = lazy(() => import('./LazyComponent')); function MyComponent() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <LazyComponent /> </Suspense> </div> ); } 复制代码预取/预加载使用预取/预加载可以在用户访问页面之前预加载页面所需的资源,从而加快页面加载时间。可以使用Vite提供的标签来实现预取/预加载。<link rel="prefetch" href="./lazy-component.js" /> 复制代码代码压缩使用代码压缩可以减小文件大小,从而加快页面加载时间。可以在Vite配置文件中启用代码压缩选项。// vite.config.ts import { defineConfig } from 'vite'; import reactRefresh from '@vitejs/plugin-react-refresh'; import { terser } from 'rollup-plugin-terser'; export default defineConfig({ plugins: [reactRefresh(), terser()], }); 复制代码图片压缩使用图片压缩可以减小图片大小,从而加快页面加载时间。可以使用Vite提供的imagemin插件来实现图片压缩。// vite.config.ts import { defineConfig } from 'vite'; import reactRefresh from '@vitejs/plugin-react-refresh'; import { imagemin } from 'rollup-plugin-imagemin'; export default defineConfig({ plugins: [ reactRefresh(), imagemin({ plugins: [ // add imagemin plugins here ], }), ], }); 复制代码缓存策略使用缓存策略可以减少重复的网络请求,从而加快页面加载时间。可以在Vite配置文件中配置缓存策略选项。// vite.config.ts import { defineConfig } from 'vite'; import reactRefresh from '@vitejs/plugin-react-refresh'; export default defineConfig({ plugins: [reactRefresh()], build: { // set cache options here cacheDir: '.vite-cache', }, }); 复制代码后记分享是一种态度。参考资料:vite-pluginvite-githubvite-build全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。
先认识它,再驾驭它大家好,我是柒八九。ChatGPT知道吧!现在最新的new Bing中已经接入了AI功能。而能够实现上述让人欲罢不能的功能。OenAI是永远绕不开的话题。而OpenAI 是一家人工智能研究机构,他们在 2020 年推出了一款基于 WebAssembly 的 AI 模型推理引擎,名为 Microscope。Microscope 可以在现代浏览器中运行,提供了高效的 AI 模型推理能力。既然,AI的模型,我们搞不定;那么WebAssembly这种更贴近前端开发者的技术,我们还是可以窥探一番的。好了,天不早了,我们开始今天WebAssembly基础知识的探索之旅。你能所学到的知识点WebAssembly 是个啥? 推荐阅读指数⭐️⭐️⭐️⭐️⭐️使用 Emscripten 写一个属于你的 wasm 推荐阅读指数⭐️⭐️⭐️⭐️⭐️胶水代码 推荐阅读指数⭐️⭐️⭐️⭐️编译目标及编译流程 推荐阅读指数⭐️⭐️⭐️WebAssembly是个啥?WebAssembly(简称Wasm)是一种可以在现代Web浏览器中运行的低级字节码。它是一种可移植、大小合理和加载速度快的格式,适用于Web上的各种应用程序。WebAssembly 是一种新的编程语言,并不是JavaScript的替代品。相反,它是一种补充,可以与现有的Web技术一起使用。WebAssembly 可以被编译成 JavaScript,也可以直接在浏览器中运行。WebAssembly 也是新一代Web 虚拟机标准,可以让用各种语言编写的代码都能以接近原生的速度在Web中运行C/C++代码可以通过Emscripten工具链编译为wasm二进制文件,进而导入网页中供js调用Rust语言更是内置了对WebAssembly的支持WebAssembly 诞生背景在目前的Web应用中,JavaScript属于一家独大的地位。但是,由于JS是弱类型语言,变量类型不是固定的,在使用变量前需要判断其类型,无疑增加了运算的复杂度,降低了执行效率。为了提高JS的效率,Mozila的工程师创建了Emscripten项目,尝试通过LLVM工具链将C/C++语言编写的程序转译为JS代码,并在此过程中创建了JS子集 (asm.js)。asm.js仅包含可以预判变量类型的数值运算,有效地避免了JS弱类型变量语法带来的执行效率低的痛点。asm.js显著的提升了JS效率,获得了主流浏览器厂商的支持。并且,各大厂商决定采用二进制格式来表示asm.js模块(减少模块体积,提升模块加载和解析速度),最终演化出WebAssembly技术。Web的第四种语言见图知意,WebAssembly已经被内置到浏览器中了。同时,.wasm可以直接运行在浏览器中。作为网页开发的第四大主力开发语言。在浏览器控制台中,直接打印就可以看到WebAssembly构造函数。 WebAssembly解决的痛点下面,我们来简单复现一下,V8是如何处理JS的。V8接收到要执行的JS 源代码源代码对 V8 来说只是一堆字符串,V8 并不能直接理解这段字符串的含义V8结构化这段字符串,生成了{抽象语法树|AST},同时还会生成相关的作用域生成字节码(介于AST和机器代码的中间代码)与特定类型的机器代码无关解释器(ignition),按照顺序解释执行字节码,并输出执行结果。通过V8将js转换为字节码然后经过解释器执行输出结果的方式执行JS,有一个弊端就是,如果在浏览器中再次打开相同的页面,当页面中的 JavaScript 文件没有被修改,再次编译之后的二进制代码也会保持不变,意味着编译这一步浪费了 CPU 资源。为了,更好的利用CPU资源,V8采用JIT(Just In Time)技术提升效率:而是混合编译执行和解释执行这两种手段。JIT引入了两个编译器基线编译器如果一段代码变成了 warm,那么 JIT 就把它送到编译器去编译,并且把编译结果存储起来。优化编译器如果一个代码段变得 very hot,监视器会把它发送到优化编译器中。生成一个更快速和高效的代码版本出来,并且存储之。优化编译器最成功一个特点叫做{类型特化|Type specialization}因为JS是动态类型语言,在代码运行过程中,如果是多形态的(即调用的过程中,类型不断变化),则会为操作所调用的每一个类型组合生成一个桩。如果存在多形态的情况,无形中就会增加了JS编译执行的时间。我们可以从几个方面来描述一下,WebAssembly是如何解决现有问题的。角度方式汇编角度WebAssembly提供了一种更接近于机器码的中间表示形式,使得代码在浏览器中的执行速度更快。它允许开发者编写高性能的代码,同时保持跨平台兼容性。v8中的JITJavaScript在浏览器中通过JIT(Just-In-Time)编译器执行,但JIT编译过程需要时间,WebAssembly的二进制格式可以更快地解码和执行。这意味着WebAssembly可以减少浏览器在解析和优化代码方面的开销,从而提高性能。类型特化角度JavaScript是一种动态类型语言,这意味着在运行时需要进行类型检查和转换。WebAssembly则是静态类型的,这使得它在编译和执行时可以避免这些类型检查和转换的开销。此外,静态类型还有助于提高代码的可读性和可维护性。JVM角度WebAssembly提供了一种独立于语言和平台的虚拟机,类似于JVM,但专为Web而设计,使得各种编程语言都可以在浏览器中高效运行。WebAssembly 优点角度原因性能WebAssembly 代码执行速度接近原生代码,因为它是为快速解码和执行而设计的。安全WebAssembly 在沙箱环境中运行,保护系统资源免受恶意代码的侵害。可移植性WebAssembly 模块可以在任何支持的浏览器和平台上运行,无需修改。与 JavaScript 互操作WebAssembly 可以与 JavaScript 代码无缝协作,使得开发者可以在性能关键部分使用 WebAssembly,而在其他部分使用 JavaScript。语言支持WebAssembly 支持多种编程语言,如 C、C++、Rust 等,使得开发者可以使用熟悉的语言编写高性能 Web 应用。WebAssembly应用WebAssembly 目前已经得到了许多公司的支持和应用,以下是一些落地项目和成就的例子:Unity Technologies:Unity 是一家游戏引擎和游戏开发工具提供商,他们在 2018 年推出了一款基于 WebAssembly 的游戏引擎,名为 "Unity 2018.2"。这款引擎可以在现代浏览器中运行,提供了与原生应用程序相同的性能和功能。Fastly:Fastly 是一家内容传递网络(CDN)提供商,他们在 2019 年推出了一款名为 "Lucet" 的 WebAssembly 运行时。Lucet 可以在云端和边缘设备上运行 WebAssembly 代码,提供了比传统服务器更高的性能和可扩展性。Figma:Figma 是一款基于 Web 的界面设计工具,他们在 2020 年推出了一款名为 "FigJam" 的新产品,其中使用了 WebAssembly 技术。FigJam 可以在浏览器中实时协作,并提供了高效的图形处理能力。OpenAI:OpenAI 是一家人工智能研究机构,他们在 2020 年推出了一款基于 WebAssembly 的 AI 模型推理引擎,名为 Microscope。Microscope 可以在现代浏览器中运行,提供了高效的 AI 模型推理能力。(最近名声大噪的-ChatGPT4你是否了解呢。神器一般的存在)使用 Emscripten 写一个属于你的 wasmEmscripten是用C/C++语言开发WebAssembly应用的标准工具,是WebAssembly宿主接口事实上的标准之一。安装 EmscriptenEmscripten包含了将C/C++代码编译为WebAssembly所需的完整工具集(LLVM/Node.js/Python/Java等),不依赖于任何其他的编译器环境。可以使用emsdk命令行工具安装Emscripten。下载最新版的Pythonemsdk是一组基于Python的脚本。我们可以在Python 官网下载并安装最新版的Python。$ python --version // 3.11.2 复制代码下载emsdkPython准备就绪后,下载emsdk工具包。// 下载emsdk $ git clone https://github.com/emscripten-core/emsdk.git 复制代码安装并激活Emscripten在控制台切换到emsdk所在目录。针对MacOS或者Linux用户,可以按照下面的代码进行配置处理。$ cd emsdk 复制代码运行以下emsdk命令从GitHub获取最新工具,并将其设置为活动状态# 获取最新版本的emsdk (第一次clone项目的时候,忽略此操作) git pull # 下载按照最新的SDK工具 ./emsdk install latest # 针对当前用户,将最新的SDK设置为“激活状态” ./emsdk activate latest # 激活当前终端中的路径和其他环境变量 source ./emsdk_env.sh 复制代码上面的命令中的输出,这里就不贴图了。对于Windows用户,按照Emscripten的方法基本一致。执行代码的区别是使用emsdk.bat代替emsdk,使用emsdk_env.bat代替source ./emsdk_env.sh。emsdk.bat update # 下载按照最新的SDK工具 emsdk.bat install latest # 针对当前用户,将最新的SDK设置为“激活状态” emsdk.bat activate latest # 激活当前终端中的路径和其他环境变量 emsdk_env.bat 复制代码Note: 安装及激活 Emscripten 只需要执行一次,然后在新建的控制台中设置一次环境变量,既可使用Emscripten核心命令emccemcc 全局安装如果想要在全局范围内,使用emcc。可以使用如下步骤:vim ~/.bash_profilesource 你的emsdk安装路径/emsdk_env.sh校验安装Emscripten安装/激活且设置环境变量后,可以通过emcc -v查看版本信息。> emcc -v // 以下是控制台输出日志: emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.33 (c1927f22708aa9a26a5956bab61de083e8d3e463) clang version 17.0.0 (https://github.com/llvm/llvm-project 671eeece457f6a5da7489f7b48f7afae55327b8b) Target: wasm32-unknown-emscripten Thread model: posix InstalledDir: /Users/PersonWorkSpace/WasmWorkSpace/emsdk/upstream/bin 复制代码编码环节又到了,我们接触新语言的环节 -- 写一个hello,world程序。生成.wasm文件由于我们是用Emscripten作为案例演示,所以我们用C语言来写代码新建一个名为hello.cc的C源文件。#include <stdio.h> int main(){ printf("hello,world!\n"); return 0; } 复制代码进入控制台,执行以下命令进行编译:emcc hello.cc 复制代码在hello.cc所在的目录下得到两个文件a.out.wasm该文件为C源文件编译后形成的WebAssembly汇编文件a.out.js是Emscripten生成的胶水代码,其中包含了Emscripten的运行环境和.wasm文件的封装导入a.out.js既可自动完成.wasm文件的载入/编译/实例化、运行时初始化等工作。我们还可以使用-o选项指定emcc的输出文件emcc hello.cc -o hell.js 复制代码在hello.cc所在的目录下得到两个文件 分别为 hello.wasm 和hello.js 代码引用与原生代码不同,C/C++代码被编译为WebAssembly后是无法直接运行的。我们需要将其导入网页,通过浏览器来执行。在HTML中引用JS我们在vscode中使用emmet直接搞一个最简单的html。然后引入我们刚才生成的hello.js<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Emscripten</title> </head> <body> <script src="./hello.js"></script> </body> </html> 复制代码然后,还有一点需要注意,WebAssembly是需要通过网页发布后才可以运行。这里,我们用node写了一个最简单的服务器。const http = require("http"), fs = require("fs"), path = require("path"), url = require("url"); // 获取当前目录 let root = path.resolve(); // 创建服务器 let sever = http.createServer(function(request,response){ let pathname = url.parse(request.url).pathname; let filepath = path.join(root,pathname); // 获取文件状态 fs.stat(filepath,function(err,stats){ if(err){ // 发送404响应 response.writeHead(404); response.end("404 Not Found."); }else{ // 发送200响应 response.writeHead(200); // response是一个writeStream对象,fs读取html后,可以用pipe方法直接写入 fs.createReadStream(filepath).pipe(response); } }); }); sever.listen(7899); console.log('Sever is running at http://127.0.0.1:7899/'); 复制代码这样,我们就可以通过http://127.0.0.1:7899/hello.html访问到刚才生成的hello.js了。然后,项目的结构如下:在http://127.0.0.1:7899/hello.html的控制台,就能看到hello,world的输出结果。在Node 环境下使用WebAssembly程序也可以在Node.js 8+版本中运行。在Vite中使用如果大家对Vite熟悉的话,它是支持直接将.wasm文件引入到项目中的。这里就直接拿来主义了哈。利用vite-plugin-wasm插件进行引入处理import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import wasm from "vite-plugin-wasm"; export default defineConfig({ plugins: [react(),wasm()], }) 复制代码预编译的 .wasm 文件可以通过 ?init 来导入。默认导出一个初始化函数,返回值为所导出 wasm 实例对象的 Promise:import init from './example.wasm?init' init().then((instance) => { instance.exports.test() }) 复制代码但是呢,如果你把上面利用Emscripten生成的hello.wasm会报错。TypeError: WebAssembly.Instance(): Import #0 module="wasi_unstable" error: module is not an object or function 复制代码使用 emscripten 构建的 wasm 模块,推荐的做法是让 emscripten 生成 JS 来实现这些 API,并为你加载模块。在网页中直接使用wasm使用 WebAssembly 可以在网页中运行更快、更强大的应用程序。要在网页中使用 WebAssembly,需要遵循以下步骤:编写 WebAssembly 模块,可以使用 C/C++、Rust 等语言编写。将 WebAssembly 模块编译为 wasm 格式。在 JavaScript 中加载 wasm 模块。在 JavaScript 中调用 wasm 模块中的函数。下面是一个简单的例子,演示如何在网页中使用 WebAssembly:我们改造一下刚才的hello.cc#include <stdio.h> int add(int a, int b) { return a + b; } int main() { int a = 2; int b = 3; int result = add(a, b); printf("The sum of %d and %d is %d\\n", a, b, result); return 0; } 复制代码使用Emscripten编译器将该代码编译为WebAssembly格式。以下是一个示例命令:emcc hello.c -o hello.wasm -s WASM=1 -s EXPORTED_FUNCTIONS="['_main', '_add']" 复制代码该命令将_main和_add函数作为可导出的函数,以便在WebAssembly模块中调用它们。然后,您可以将生成的WASM文件嵌入到HTML文件中,并使用JavaScript代码调用它们。// 加载 wasm 模块 fetch('hello.wasm') .then(response => response.arrayBuffer()) .then(bytes => WebAssembly.instantiate(bytes)) .then(results => { // 调用 wasm 模块中的函数 const { add } = results.instance.exports; console.log(add(1, 2)); // 输出 3 }); 复制代码在上面的例子中,我们首先使用 fetch 函数加载 wasm 模块,然后使用 WebAssembly.instantiate 函数将其实例化。最后,我们可以通过 results.instance.exports 对象访问 wasm 模块中的函数,并在 JavaScript 中调用它们。胶水代码Emscripten在编译时,生成了大量的JS胶水代码。我们通过VScode打开hello.js发现,大多数的操作都围绕全局对象Module展开。该对象正是Emscripten程序运行的核心所在。我们可以通过vscode快捷键Ctrl+K+0将所有函数折叠起来。这样方便查看顶层函数的定义。WebAssembly汇编模块载入WebAssembly汇编模块(即.wasm)的载入是在instantiateAsync中完成的。上述代码就做了几件事尝试使用 WebAssembly.instantiateStreaming()创建wasm模块的实例如果流式创建失败,改用WebAssembly.instantiate()方法创建实例成功实例化后返回值交由receiveInstance方法处理在receiveInstance中执行了下面的指令:Module['asm'] = exports; 复制代码意思就是将wasm模块实例的导出对象传给Module的子对象asm。异步加载WebAssembly实例是通过WebAssembly.instantiateStreaming()或WebAssembly.instantiate()方法创建的,而这两个方法均为异步调用,这就意味着.js加载完成时,Emscripten的运行时并未准备就绪。就会出现在载入hello.js后,立即调用Module._main()会报错。解决这一问题需要建立一种运行时准备就绪的通知机制。我们可以使用onRuntimeInitialized回调。<body> <script> Module ={}; Module.onRuntimeInitialized = function(){ Module._main(); } </script> <script src="./hello.js"></script> </body> 复制代码基本思路就是在Module初始化前,向Module中注入一个名为onRuntimeInitialized的方法,当Emscripten的运行时准备就绪时,将会回调该方法。在hello.js中的run()中调用了onRuntimeInitialized编译目标及编译流程Emscripten可以设定两种不同的编译目标WebAssemblyasm.js编译目标的选择以asm.js为编译目标时,C/C++代码被编译为.js文件;以WebAssembly为编译目标时,C/C++代码被编译为.wasm文件及对应的.js胶水代码文件。二者在实际应用中主要区别在于模块加载的同步还是异步:以asm.js为编译目标时,由于C/C++代码被完全转换成asm.js(JS子集),因此认为模块是同步加载的以WebAssembly为编译目标时,由于WebAssembly的实例化方法本身是异步指令,因为认为模块是异步加载的在兼容性允许的情况下,应尽量以WebAssembly为编译目标编译流程C/C++代码通过Clang编译为LLVM字节码,然后根据不同的目标编译为asm.js或wasm。后记分享是一种态度。参考地址emscripten.orgWebAssembly面向WebAssembly编程全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。
拖延,是对自我的孤立与放逐大家好,我是柒八九。在前端技术,如雨后春笋般破土而出的今天。其技术偏向性,不仅仅是搞一个新的技术框架,更多的是往高性能和底层技术发展。比方说,利用Rust特性所编写的SWC前端构建工具,目前核心功能相当于 Babel;还有就在2022年10月26日,Vercel 公司正式宣布推出新的打包工具 Turbopack,他们用基于 Rust 的 SWC 替换基于 JavaScript 的转译器 Babel,速度提升了 17 倍。他们还替换了 Terser,压缩的速度提高了 6 倍,从而减少了加载时间和带宽的使用;还有在一些原本只能在客户端运行的程序,现在也被移植到浏览器中运行,例如AutoCAD/Photoshop等,而这些都依赖近期比较热门的WebAssembly技术。从上面的最新的技术上来看,前端后续的发展都慢慢的往编译优化,重度应用可移植等方向发展。而如果继续探究上面发生的事,其实在这些新技术风向标所显示的信息,都和一个技术语言相关,那就是Rust。Rust站在了前人的肩膀上,借助于最近几十年的语言研究成果,创造出了所有权与生命周期等新的概念。相对于C/C++等传统语言,它具有天生的安全性;同时相对于C#/Java/JS等带有垃圾回收的语言来讲,它遵循了{零开销抽象| Zero-Cost Abstraction}规则,并为开发者保留了最大的底层控制能力。俗话说,站在风口上,猪都会飞。既然,上面所说的是大势所趋,那我们为什么不尝试一下。而针对已经在工作的人来说,Rust也是有一定的诱惑力。 将Rust用生产环境并用它来处理各式各样的任务。这些任务包括命令行工具开发、Web服务器、DevOps工具开发、嵌入式设备开发、音频图像分析转码、数字货币交易、搜索引擎开发、以及将其转换成WebAssembly在浏览器上发光发热。所以今天,我们又准备开辟一个新的知识体系 --Rust学习笔记。老话说的好,不想当将军的士兵不是好士兵。但是,在你想成为将军的时候,你需要拥有成为将军的知识储备和能力。这也是我们常说的未雨绸缪。该系列文章的第一篇文章,我们来讲讲Rust环境配置和入门介绍的常规知识。好了,天不早了,干点正事哇。 你能所学到的知识点在macOS环境中安装Rust 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️构建一个Rust应用 推荐阅读指数 ⭐️⭐️⭐️编译和运行是两个不同的步骤 推荐阅读指数 ⭐️⭐️⭐️如何使用Cargo构建Rust应用 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️在macOS环境中安装Rust通过一个叫rustup的命令行工具来完成Rust的下载和安装,这个工具被用来管理不同的Rust发行版本及其附带工具链。由于本人电脑是macOS环境,所以我们后续的所有示例介绍和操作都基于macOS。打开命令行终端,并输入命令:curl https://sh.rustup.rs --sSf |sh 复制代码这条命令会下载并执行一个脚本来安装rustup工具,进而安装最新的Rust稳定版本。上述的安装过程会自动将Rust工具链添加到环境变量PATH中,并在下一次登录终端时生效。如果你想要立即开始使用Rust而不用重新启动终端,可以在终端中运行如下所示的命令来让配置立即生效:source $HOME/.cargo/env 复制代码或者也可以向~/.bash_profile文件中添加下面的语句,手动将Rust添加到环境变量PATH中export PATH="$HOME/.cargo/bin:$PATH" 复制代码为了正常编译执行Rust程序,还需要一个{链接器| Linker}。由于C语言编译器通常会附带运行正常的链接器,并且一部分常用的Rust包会依赖于C语言编写的代码,所以为了能正常编译运行Rust,C语言编译器是必要部分。常用命令rustup update :来更新Rust版本rustup self uninstall : 卸载rustup及Rust工具链rustc --version:检查Rust是否被正确安装,如果一切正常,在命令输出就会看到格式为最新稳定版本的版本号、当前版本的hash、版本的提交日期rustc 1.64.0 (a55dd71d5 2022-09-19)rustup doc: 在浏览器中打开在安装工具再执行过程中在本地生成的离线文档Hello,Rust我们还是继续用最原始的方式,来了解一个新语言。用对应的语法写一个Hello,Rust。创建一个文件夹打开终端并输入相应的命令,来创建文件夹及第一个Hello,Rust项目。mkdir ~/projects cd ~/projects mkdir hello_rust cd hello_rust 复制代码编写并运行Rust程序创建一个名为main.rs的源文件。在命令规则上,Rust文件总是以.rs扩展名结尾。fn main(){ println!("Hello,Rust"); } 复制代码然后保存文件并回到终端窗口。 在macOS环境下,可以通过如下的命令编译并运行对应的文件。rustc main.rs ./main 复制代码输出为:Hello,Rust。程序剖析第一个值得注意的部分如下所示fn main(){ } 复制代码这部分代码定义了Rust的一个函数。这里的main函数比较特殊:当你运行一个可执行Rust程序时,所有的代码都会从这个入口函数开始运行。这段代码的第一行声明了一个名为main的、没有任何参数和返回值的函数。那对花括号{}被用来标记函数体,Rust要求所有的函数都要被花括号包裹起来。再来看main函数体中的代码println!("Hello,Rust"); 复制代码这一行代码的作用是:将字符串输出到终端上。这里我们调用了一个被叫住println!的宏。针对宏的解释,我们后面会有详细的分析。这里只要记住,Rust中所有以!结尾的调用都意味着你正在使用一个宏而不是普通函数。编译和运行是两个不同的步骤在运行一段Rust程序之前,必须输入rustc命令及附带的源文件名参数来编译它:rustc main.rs 复制代码这过程和C/C++的gcc或clang编译非常相似。一旦编译成功,就会获得一个二进制的可执行文件。上面的步骤,其实和我们平时使用js是不一样的。js是动态语言,在编译之后就会立即运行。而Rust是一种预编译语言,这意味着当你编译完Rust程序之后,便可以将可执行文件交付他人,并运行在没有安装Rust的环境中。Hello,Cargo每次运行rustc都比较繁琐,项目小还可以忍受吗,但是如果随着项目增大,这无疑是一种折磨。所以,在实际运用中,我们用Rust构建工具:CargoCargo是Rust工具链中内置的构建系统及包管理器。由于它可以处理众多诸如构建代码、下载编译依赖库等繁琐但重要的任务,所以绝大部分的Rust用户都选择它来管理自己的Rust项目。如果是通过curl https://sh.rustup.rs --sSf |sh来安装Rust,那么Cargo就已经被附带在了当前的Rust工具链里。我们可以通过如下命令来检查Cargo是否被安装妥当cargo --version 复制代码用Cargo 创建一个项目我们还是在projects文件夹下运行。cargo new hello_cargo cd hello_cargo 复制代码第一条命令会创建名为hello_cargo项目。Cargo会以hello_cargo的名字来创建项目目录并放置它生成的文件。当我们进入hello_cargo文件夹,会看到Cargo刚刚生成的两个文件与一个目录一个名为Cargo.toml的文件一个名为main.rs的源代码文件,该源代码文件被放置在src目录下与此同时,Cargo还好初始化一个新的git仓库并生成默认的.gitignore文件忽略.git目录Cargo.toml使用一个文本编辑器打开Cargo.tomlCargo使用TOML(TOM's Obvious,Minimal Language)作为标准的配置格式。首行文本中的[package]是一个区域标签,它表明接下来的语句会被用于当前的程序包。紧随标签后的3行语句提供了Cargo编译这个程序时需要的配置信息,它们分别是程序名- hello_cargo程序版本号 - 0.1.0Rust版本号 - 2021最后一行文本中[dependencies]同样是一个区域标签,它表明随后的区域会被用来声明项目的依赖。在Rust中,把代码的集合称为{包| Crate}。crate是Rust中最小的编译单元,package是单个或多个crate。其他Cargo为我们生成了一个输出Hello,Rust的程序。并且源文件main.rs被放置到了src目录下,在项目目录下多了一个叫Cargo.toml的配置文件。Cargo会默认把所有的源代码文件保存到src目录下,而项目根目录只被用来存储诸如README文档/许可声明/配置文件等与源代码无关的文件。使用Cargo构建和运行项目在hello_cargo项目目录下,Cargo可以通过下面的命令来完成构建任务。cargo build 复制代码并通过./target/debug/hello_cargo完成程序运行。继续使用tree来查看文档目录首次使用命令cargo build构建的时候,它会在项目根目录下创建一个名为Cargo.lock的新文件,这个文件记录了当前项目所有依赖库的具体版本号。不要手动编辑其中的内容,Cargo可以帮助你自动维护它。当然,我们可以把上述build和手动查找并执行debug目录下的可执行文件的两个操作合并成一个操作。cargo run 复制代码cargo run命令依次完成编译和执行任务。此外,Cargo提供了一个叫做cargo check的命令,用来快速检查当前代码是否可以通过编译,而不需要花费额外的时间去真正生成可执行程序。以Release 模式进行构建当准备好发布自己的项目时,可以使用命令cargo build --release在优化模式构建并生成可执行程序。它生成的可执行文件会被放置在target/release目录下,而不是之前的target/debug目录下。这种模式会以更长的编译时间为代价来优化代码,从而使代码拥有更好的运行时性能。后记分享是一种态度。参考资料:《Rust权威指南》全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。
欲望越大,我们需要的奔跑速度就越快;而筋疲力尽之时,便是我们幸福感滑坡之时大家好,我是柒八九。今天,我们又开辟了一个新的篇幅 --前端面试。即是把一些平时常用的概念和工具方法整理和罗列,也算是一种变向的未雨绸缪。该系列的文章,大部分都是前面文章的知识点汇总,但是也不乏参考其他优秀文章。不过,大家可以放心,里面的代码和知识点,都有迹可循。好了,天不早了,干点正事哇。 你能所学到的知识点选择器流、元素盒模型元素超出宽度...处理元素隐藏层叠规则块级格式化上下文元素居中flex布局Chrome支持小于12px 的文字CSS 优化处理 (6个)回流、重绘硬件加速Css预编译语言选择器选择器(.#[]:::)5个瞄准目标元素类选择器以.开头ID选择器#开头权重相当高ID一般指向唯一元素属性选择器含有[]的选择器[title]{}/[title="test"]{}伪类选择器前面有一个冒号(:)的选择器:link :选择未被访问的链接:visited:选取已被访问的链接:active:选择活动链接:hover :鼠标指针浮动在上面的元素伪元素选择器有连续两个冒号(::)的选择器::before : 选择器在被选元素的内容前面插入内容::after : 选择器在被选元素的内容后面插入内容关系选择器 (空格>~+)4个根据与其他元素的关系选择元素的选择器后代选择器选择所有合乎规则的后代元素空格链接相邻后代选择器仅仅选择合乎规则的儿子元素孙子,重孙子元素忽略>链接兄弟选择器选择当前元素后面的所有合乎规则的兄弟元素~链接相邻兄弟选择器仅仅选择当前元素相邻的那个合乎规则的兄弟元素+链接常见的使用场景是,改变紧跟着一个标题的段的某些表现方面权重!important (10000)内联(1000)ID选择器(0100)类选择器(0010)标签选择器(0001)上面的优先级计算规则,内联样式的优先级最高,如果外部样式需要覆盖内联样式,就需要使用!important流、元素块级元素常见的块级元素块级元素和display为block的元素不是一个概念元素默认的display值是list-item元素默认的display值是table基本特征:一个水平流上只能单独显示一个元素,多个块级元素则换行显示由于块级元素具有换行特性,配合clear属性用来清除浮动.clear::after{ content:''; display:table; //或者list-item clear:both; } 复制代码盒子每个元素都有两个盒子外在盒子负责元素是可以一行显示,还是只能换行显示内在盒子负责宽高、内容呈现按照display的属性值不同block外在盒子:块级盒子内在盒子:块级容器盒子inline-block外在盒子:内联盒子内在盒子:块级容器盒子inline外在盒子:内联盒子内在盒子:内联盒子可以粗略的认为:display:block ≈ display:block-block display:inline≈ display:inline-inline 复制代码块级盒子负责结构,内联盒子负责内容内联元素如何区分内联元素从定义上:内联元素的内联特指外在盒子从表现上:可以和文字在一行显示幽灵空白节点在H5文档声明中,内联元素的所有解析和渲染表现就,如同每个行框盒子的前面有一个空白节点一样,这个空白节点永远透明,不占据任何宽度。表现如同文本节点一样。幽灵空白节点也是一个盒子,但是一个假想盒,名为strut。一个存在于每个行框盒子前面,同时具有该元素的字体和行高属性的0宽度的内联盒行框盒子(line box),每一行就是一个行框盒子,每个行框盒子又是由一个个内联盒子组成的。盒模型一个盒子由四个部分组成:content、padding、border、margincontent,即实际内容,显示文本和图像content 属性大都是用在::before/::after这两个伪元素中padding,即内边距,内容周围的区域内边距是透明的取值不能为负受盒子的background属性影响padding 百分比值无论是水平还是垂直方向均是相对于宽度计算boreder,即边框,围绕元素内容的内边距的一条或多条线,由粗细、样式、颜色三部分组成margin,即外边距,在元素外创建额外的空白,空白通常指不能放其他元素的区域标准盒模型盒子总宽度 = width + padding + border + margin;盒子总高度 = height + padding + border + margin也就是,width/height 只是内容宽高,不包含 padding 和 border 值IE 怪异盒子模型盒子总宽度 = width + margin;盒子总高度 = height + margin;也就是,width/height 包含了 padding 和 border 值更改盒模型CSS 中的 box-sizing 属性定义了引擎应该如何计算一个元素的总宽度和总高度box-sizing: content-box|border-box 复制代码content-box (默认值),元素的 width/height 不包含padding,border,与标准盒子模型表现一致border-box 元素的 width/height 包含 padding,border,与怪异盒子模型表现一致唯一离不开box-sizing:border-box的就是:原生普通文本框和文本域</code>的100%自适应父容器宽度</div><div data-lake-id="067eebcb44ce0bd1a26ac3b040809e32">替换元素的特性之一:尺寸由<em>内部元素</em>决定并且无论其<code>display</code>属性值是<code>Inline</code>还是<code>block</code>也就是说替换元素的宽度却不受<code>display</code>水平影响</div><div data-lake-id="fee4fb1c315ba76dd8e46016b86fc2d9">而<code><textarea>/<input></code>就是替换元素,修改<code><textarea></code>的<code>display</code>为<code>block</code>是无法让尺寸100%自适应父容器。</div><div data-lake-id="b61c86de22c7ae248813f39139b0a709">通过设置<code><textarea></code>的width为100%,自适应父容器。</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22textarea%7B%5Cn%20width%3A100%25%3B%5Cn%20box-sizing%3Aborder-box%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22YTTNX%22%7D"></div><blockquote style="background-color: #FFF9F9;"><div data-lake-id="a1053e88b7690b464a4c5064b7c002eb">设计初衷:解决<strong>替换元素</strong>宽度自适应问题</div></blockquote><div data-card-type="block" data-ready-card="hr"></div><h1 data-lake-id="e305034a0f641dd58b62a2744865698f" id="4k54Z" style="text-align: center;"><br /></h1><h1 data-lake-id="58f18fb14f1784c7dd832b6568e7c0c4" id="h98qk" style="text-align: center;">元素超出宽度...处理</h1><div data-lake-id="df71274ba9b9e4562be34de64552dd56" style="text-align: center;"><br /></div><h2 data-lake-id="6cf16182f414e7c8ac011efbcc46bdb6" id="tc9DO">单行 (AKA: TWO)</h2><div data-lake-id="13e5947ef852ba608c16696a7605e72e"><br /></div><ol data-lake-id="eec0285d84ceddf78dbc27385f293a47"><li data-lake-id="602ed8472fde0d534cda4546a5a84b4b" style="padding-left: 6px;"><code>text-overflow:ellipsis</code>:当文本溢出时,显示省略符号来代表被修剪的文本</li><li data-lake-id="eb66bb38cf43a17682f998ba5bf2cbb9" style="padding-left: 6px;"><code>white-space:nowrap</code>:设置文本不换行</li><li data-lake-id="2577832b15784225fd69ef528a10ced3" style="padding-left: 6px;"><code>overflow:hidden</code>:当子元素内容超过容器宽度高度限制的时候,裁剪的边界是<code>border box</code>的内边缘</li><li data-lake-id="a6450af8347e78fe8f061fee293b062e" style="padding-left: 6px;">用三个属性的首字母就是:<code>TWO</code></li></ol><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22p%7B%5Cn%20%20%20%20text-overflow%3A%20ellipsis%3B%5Cn%20%20%20%20white-space%3A%20nowrap%3B%5Cn%20%20%20%20overflow%3A%20hidden%3B%5Cn%20%20%20%20width%3A400px%3B%5Cn%20%20%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22DmAlb%22%7D"></div><h2 data-lake-id="30c468100464e61033ecb619680e44d8" id="dipbw"><br /></h2><h2 data-lake-id="569c22369edafc3e151e678dd70b41dc" id="QJ9cC">多行</h2><div data-lake-id="4d744b23c4887a692bb9d77f8d2d1f00"><br /></div><ol data-lake-id="391958e6db9c668a63df285e47cf5eb6"><li data-lake-id="99280caff699b5d0efd6742aeba672c1" style="padding-left: 6px;">基于高度截断(伪元素 + 定位)</li><li data-lake-id="660f3f1413a3c6d3f7943cc160defe96" style="padding-left: 6px;">基于行数截断()</li></ol><h3 data-lake-id="2b252ee02470858728f49d88ed87a711" id="PnkNt" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="7ebf8cd014acf69dae0f4a2827749cc2" id="SMFDh" style="padding-left: 9px;">基于高度截断</h3><div data-lake-id="7c8bbf633594700d16339fe0f0c3d90d" style="padding-left: 9px;"><br /></div><div data-lake-id="6d51c5c9a0056654160d35be22e2a5bd">关键点 <code>height + line-height + ::after + 子绝父相</code></div><div data-lake-id="19c11ea825d4159d08d88dd90e74e259">核心的css代码结构如下:</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.text%20%7B%5Cn%20%20%20%20position%3A%20relative%3B%5Cn%20%20%20%20line-height%3A%2020px%3B%5Cn%20%20%20%20height%3A%2040px%3B%5Cn%20%20%20%20overflow%3A%20hidden%3B%5Cn%20%20%7D%5Cn.text%3A%3Aafter%20%7B%5Cn%20%20%20%20content%3A%20%5C%22...%5C%22%3B%5Cn%20%20%20%20position%3A%20absolute%3B%5Cn%20%20%20%20bottom%3A%200%3B%5Cn%20%20%20%20right%3A%200%3B%5Cn%20%20%20%20padding%3A%200%2020px%200%2010px%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22WuH2J%22%7D"></div><h3 data-lake-id="3bd0dd55f7f696c49d1ce039bb6b979e" id="NAAfJ" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="bbb56b4a07566ed779bda29ba1491050" id="sKrh9" style="padding-left: 9px;">基于行数截断</h3><div data-lake-id="ba0cb4fea80ab2aaf1a866c324d31b7d" style="padding-left: 9px;"><br /></div><div data-lake-id="2ad51e97a2314bcc9fb8973424de6f0f">关键点:<code>box + line-clamp + box-orient</code> + <code>overflow</code></div><ol data-lake-id="2b74a84251677abd24e03d8b5071a6a2"><li data-lake-id="88c898afa924a64c6c4b743f633d666e" style="padding-left: 6px;"><code>display: -webkit-box</code>:将对象作为<strong>弹性伸缩盒子模型</strong>显示</li><li data-lake-id="26a5c560dac5698973363575cd69838c" style="padding-left: 6px;"><code>-webkit-line-clamp: n</code>:和①结合使用,用来限制在一个块元素显示的文本的行数(<code>n</code>)</li><li data-lake-id="b7625450838627fa568c61c59d6d9b59" style="padding-left: 6px;"><code>-webkit-box-orient: vertical</code>:和①结合使用 ,设置或检索伸缩盒对象的子元素的排列方式</li><li data-lake-id="ebe32ed19c0b2d1f61e0c227fc94a716" style="padding-left: 6px;"><code>overflow: hidden</code></li></ol><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%3Cp%3E%5Cn%20%20In%20this%20example%20the%20%3Ccode%3E-webkit-line-clamp%3C%2Fcode%3E%20property%20is%20set%20to%5Cn%20%20%3Ccode%3E3%3C%2Fcode%3E%2C%20which%20means%20the%20text%20is%20clamped%20after%20three%20lines.%20An%20ellipsis%5Cn%20%20will%20be%20shown%20at%20the%20point%20where%20the%20text%20is%20clamped.%5Cn%3C%2Fp%3E%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22LCNNw%22%7D"></div><div data-lake-id="95077831e4b49df3779ef097ded2fd94">css</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22p%20%7B%5Cn%20%20width%3A%20300px%3B%5Cn%20%20display%3A%20-webkit-box%3B%5Cn%20%20-webkit-box-orient%3A%20vertical%3B%5Cn%20%20-webkit-line-clamp%3A%203%3B%5Cn%20%20overflow%3A%20hidden%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%229Lgrq%22%7D"></div><div data-lake-id="d5b69a02a52cdfc3bef9cc5beee1b6e9"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_aeb68dd103004b33abbffbcfb6c622af.png%22%2C%22originWidth%22%3A670%2C%22originHeight%22%3A212%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A670%2C%22height%22%3A212%7D"></span></div><div data-card-type="block" data-ready-card="hr"></div><h1 data-lake-id="68b4a8e688e37f470d2b7de8cd0b45cc" id="8epWJ" style="text-align: center;"><br /></h1><h1 data-lake-id="4719c1a34d59ed58108c09ff4d9150f6" id="yfQgv" style="text-align: center;">元素隐藏</h1><div data-lake-id="983370357c2c951724077eb1aa563595" style="text-align: center;"><br /></div><div data-lake-id="39482c6eeb71c908b44493ea8e770dec">可按照隐藏元素<strong>是否占据空间</strong>分为<strong>两大类</strong>(6 + 3)</div><ol data-lake-id="7eba0c3ee850dc44e6c96a7eaa237cf9"><li data-lake-id="a3e6e211f81e5a69f38a65c44c8da979">元素不可见,不占空间(<code>3absolute</code>+<code>1relative</code>+<code>1script</code>+<code>1display</code>)</li></ol><ol data-lake-id="90228b7b5d6aecafc51139d8b92feb61" data-lake-indent="1"><li data-lake-id="911cc98247aa41bd19d94e9c48467eb3" style="padding-left: 6px;"><code><script></code></li><li data-lake-id="2fc108b88cecfebd166bbdc4b597e2d9" style="padding-left: 6px;"><code>display:none</code></li><li data-lake-id="5d7ba1ea14649f15404df5837a364ff6" style="padding-left: 6px;"><code>absolute</code> + <code>visibility:hidden</code></li><li data-lake-id="70ba662cc331fe95b3d9444b86ad03ff" style="padding-left: 6px;"><code>absolute</code> + <code>clip:rect(0,0,0,0)</code></li><li data-lake-id="4c6bbc309e1faa16555cb9670b663274" style="padding-left: 6px;"><code>absolute</code> + <code>opacity:0</code></li><li data-lake-id="894897a095a97577d5b91b2a41665eec" style="padding-left: 6px;"><code>relative</code>+<code>left</code>负值</li></ol><ol data-lake-id="2c2c4640a7751fb6b5073823f80c8708" start="2"><li data-lake-id="9a0d9253b73a447a903d0942b1bbebb0">元素不可见,占据空间(3个)</li></ol><ol data-lake-id="bed978cafba24cabb787c32d02324937" data-lake-indent="1"><li data-lake-id="db47dc01effb7f23aea868ae3999ecbc" style="padding-left: 6px;"><code>visibility</code>:<code>hidden</code></li><li data-lake-id="b6299dc5ca5a897fb2f3e34842e74b12" style="padding-left: 6px;"><code>relative</code> + <code>z-index</code>负值</li><li data-lake-id="03408093143bba4b4cf1701e34ce20fd" style="padding-left: 6px;"><code>opacity:0</code></li></ol><h2 data-lake-id="19753e276bd875eb81485a9d5518e0a9" id="N2Oa5"><br /></h2><h2 data-lake-id="9c0bf228af64aaf40d80f0ac7b0a310d" id="F1gVN">元素不可见,不占空间</h2><div data-lake-id="82cc05816007421cdbc6c3f1266e5236"><br /></div><h3 data-lake-id="cb2b7dbed6963808acfa8a9db9c6af84" id="a7xqr" style="padding-left: 9px;"><code><br /></code></h3><h3 data-lake-id="f8adc2fbdfccc290673482cafcc00d1e" id="ttiEd" style="padding-left: 9px;"><code><script></code></h3><div data-lake-id="28812514bb3814b0fce1573eb180c416" style="padding-left: 9px;"><br /></div><div data-lake-id="d146fd568e273be00d8496a7ce9e6abc">其他特点:辅助设备无法访问,同时不渲染</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%3Cscript%20type%3D%5C%22text%2Fhtml%5C%22%3E%5Cn%20%20%3Cimg%20src%3D%5C%221.jpg%5C%22%3E%5Cn%3C%2Fscript%3E%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%224sqcZ%22%7D"></div><h3 data-lake-id="db829d847d968b6062aebe728f82c5d7" id="hhcPa" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="22d28b44fa1623197357d1d14625d620" id="HoEdu" style="padding-left: 9px;">display:none</h3><div data-lake-id="2bbac0dcd550da5d07a560f6016ec0f7" style="padding-left: 9px;"><br /></div><div data-lake-id="652550ec40331c29bd4b08c970b06971">其他特点:辅助设备无法访问,<strong>资源加载,DOM可访问</strong></div><div data-lake-id="bf4e20b1eced87ddd89aea7e0f4b3d0e">对一个元素而言,如果<code>display</code>计算值是none,则该元素以及所有后代元素都隐藏</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.hidden%20%7B%5Cn%20%20%20display%3Anone%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22YAPMA%22%7D"></div><h3 data-lake-id="ddda83ed581db8d3b84b7f48012eca12" id="43HNc" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="807c0ba319e748eaed4d460e630de06d" id="9WJ2C" style="padding-left: 9px;">absolute + visibility</h3><div data-lake-id="f7798fba71850de52cb76dee532d8d0a" style="padding-left: 9px;"><br /></div><div data-lake-id="ac624d578ddff90a898627f7e3e99cd0">其他特点:辅助设备无法访问,但显隐的时候有<code>transition</code>效果</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.hidden%7B%5Cn%20%20position%3Aabsolute%3B%5Cn%20%20visibility%3Ahidden%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22bNAru%22%7D"></div><h3 data-lake-id="eeb56d489744725a7583e2b7f7fdbed0" id="opZSF" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="dc9ae433986a7923ab549399b7f4a924" id="spUba" style="padding-left: 9px;">absolute + clip</h3><div data-lake-id="fe81e14853a22380f7c3dc81b9c2ca06" style="padding-left: 9px;"><br /></div><div data-lake-id="2b405373bc1e682457d5a5735cdd3864">其他特点:不能点击,但<strong>键盘可访问</strong></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.hidden%7B%5Cn%20position%3Aabsolute%3B%5Cn%20clip%3Arect(0%2C0%2C0%2C0)%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%2267O4p%22%7D"></div><h3 data-lake-id="f5f4440536e68963da2934268bf2aa11" id="rXhZ6" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="c2aefa65484521056f74a14196f37d88" id="e0aWa" style="padding-left: 9px;">absolute + opacity</h3><div data-lake-id="a9b2bf3f6e6c071645585902771e945d" style="padding-left: 9px;"><br /></div><div data-lake-id="2645b0f224f2b7f727cc36ca30021bae">其他特点:可点击</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.hidden%7B%5Cn%20position%3Aabsolute%3B%5Cn%20opacity%3A0%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22zTbQP%22%7D"></div><h3 data-lake-id="c62471023de9a3623aa1c27062e08384" id="pD7X0" style="padding-left: 9px;"><code><br /></code></h3><h3 data-lake-id="170b3b39eadf38bfaf043706a16cf00c" id="v6EHp" style="padding-left: 9px;"><code>relative</code>+负值</h3><div data-lake-id="e053bc9841344c3bc33a269f1e5421e7" style="padding-left: 9px;"><br /></div><div data-lake-id="bf2b805e4f92d2b75c934dc3f4ba2895">其他特点:不能点击,但键盘可访问</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.hidden%7B%5Cn%20position%3Arelative%3B%5Cn%20left%3A-999em%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22iEOtg%22%7D"></div><h2 data-lake-id="8839ef68e878edcdccaa7addd59dbe16" id="h4IUu"><br /></h2><h2 data-lake-id="81de77bb10a6250381d3a8b67004d850" id="oCIFc">元素不可见,占据空间</h2><div data-lake-id="ab76f24d9dfc30615232e6e341b710ba"><br /></div><h3 data-lake-id="e9a6e3292f2c6612bb0cde45ed0f698b" id="EYj2G" style="padding-left: 9px;">visibility:hidden</h3><div data-lake-id="3c03745fdaf03db3da47feecbb919453" style="padding-left: 9px;"><br /></div><div data-lake-id="ea16b963bd92914ff574848a0e1b49c0">其他特点:不能点击,辅助设备无法访问</div><div data-lake-id="b7f532383c647db83e1ed0cb5e2de1d8"><code>visibility</code> 的继承性</div><ul data-lake-id="59307ae3d8ffa8cd19af78b2986fc0b1"><li data-lake-id="2c616ab6d946fe8a6c3eed39e6db1c7d">父元素设置<code>visibility:hidden</code>,子元素也看不见</li><li data-lake-id="2cde0edd9241502d76cf7e65408005d0">但是,如果子元素设置了<code>visibility:visible</code>,则<strong>子元素又会显示出来</strong></li></ul><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.hidden%7B%5Cn%20visibility%3Ahidden%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22Cgyr2%22%7D"></div><h3 data-lake-id="dfccace4e52c8706df134221d0963cf0" id="YGvTX" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="3b7d2c9b047ae587fb81e40a868d4a00" id="3CSiY" style="padding-left: 9px;">relative + z-index</h3><div data-lake-id="b592a51ad06f79f5913c11b906728b24" style="padding-left: 9px;"><br /></div><div data-lake-id="0846bdadceb023e9caf68f55547eb8be">其他特点:不能点击,键盘可访问</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.hidden%7B%5Cn%20position%3Arelative%3B%5Cn%20z-index%3A-1%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22Mi2io%22%7D"></div><h3 data-lake-id="1e466212f0087f7ad5662f402d4524ed" id="vr8N7" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="71c0c75f2ad2bf6a4fbdd52492c62e10" id="WKQdf" style="padding-left: 9px;">opacity:0</h3><div data-lake-id="98b7b8bdd49d571a4f21090843992aaa" style="padding-left: 9px;"><br /></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.hidden%7B%5Cn%20opacity%3A0%3B%5Cn%20filter%3AAlpha(opacity%3D0)%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22nVshc%22%7D"></div><h2 data-lake-id="7c1d758cd74f6159f754691b86cde70c" id="TrmDS"><br /></h2><h2 data-lake-id="321da3f6227c5a29f43baf2882e9d137" id="jHaHu">总结</h2><div data-lake-id="c3e0d67cbef96644e42bf9309dd5de69"><br /></div><div data-lake-id="4c761605b85d922a89f34093078a128a">最常用的还是<code>display:none</code>和<code>visibility:hidden</code>,其他的方式只能认为是奇招,它们的真正用途并不是用于隐藏元素,所以并不推荐使用它们。</div><div data-lake-id="0d71d888f2d5bd10e829ca7c1cb4bbe0">关于<code>display: none</code>、<code>visibility: hidden</code>、<code>opacity: 0</code>的区别,如下表所示:</div><div data-lake-id="418f35f9eb992ff76ddffcabde3b990f"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_a2a900497420446fa82e5a5f694bf82c.png%22%2C%22originWidth%22%3A1814%2C%22originHeight%22%3A998%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A1814%2C%22height%22%3A998%7D"></span></div><div data-card-type="block" data-ready-card="hr"></div><h1 data-lake-id="45c3a822a580f5be11151b84ed954755" id="FLLGf" style="text-align: center;"><br /></h1><h1 data-lake-id="9b6476fd25c2e3246bf02e4cea68ef0c" id="hL25z" style="text-align: center;">层叠规则</h1><div data-lake-id="f7aaf92e1e1440df22ffd18b8b06db7e" style="text-align: center;"><br /></div><div data-lake-id="0ddb4d76ed0a2ad59534274ba547c8a6">所谓层叠规则,指的是当网页中的元素发生层叠时的表现规则。</div><blockquote style="background-color: #FFF9F9;"><div data-lake-id="6b30408a7387f59e28d93cc183ef6548"><code>z-index</code>:<code>z-index</code>属性只有和<strong>定位元素</strong>(<code>position</code>不为<code>static</code>的元素)在一起的时候才有作用。</div></blockquote><div data-lake-id="615fba81904365b756d8836433fd976d"><code>CSS3</code>中,<code>z-index</code>已经并非只对定位元素有效,<code>flex</code>盒子的<strong>子元素</strong>也可以设置<code>z-index</code>属性。</div><h2 data-lake-id="a868d4d4d02034ccd32db58b71ee0f13" id="ICndK"><span><br /></span></h2><h2 data-lake-id="d96856dde8b8fcd084289a7ae63dbead" id="olLSi"><span>{层叠上下文|Stacking Context}</span></h2><div data-lake-id="f2744268a0f3fb0f100f2c0557787e25"><span><br /></span></div><div data-lake-id="f761b5c3cfbee10121d7613ea48bf601"><span>{层叠上下文|Stacking Context}</span>是HTML中一个三维概念,如果一个元素含有层叠上下文,可以理解这个元素在<strong>Z轴</strong>上高人一等。</div><h3 data-lake-id="7311b9be99894e9e7642693b4b894abc" id="I2Lkw" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="a2dbfa9918719d91e962a50a63e840c7" id="d9aP8" style="padding-left: 9px;">层叠上下文的特性</h3><div data-lake-id="65f264cf2ab3063a49e2aad78767484e" style="padding-left: 9px;"><br /></div><ul data-lake-id="0cb8ff327389a842bba767bb86278ef7"><li data-lake-id="1df8cbcfc707ca0b36073f5d246ac382">层叠上下文的层叠水平要比普通元素高</li><li data-lake-id="4e8cdf24420950903cc79cd89ba95e50">层叠上下文可以阻断元素的混合模式</li><li data-lake-id="6ea5e35169b87babaabf59de522b3063"><strong>层叠上下文可以嵌套,内部层叠上下文及其所有元素均受制于外部的层叠上下文</strong></li><li data-lake-id="6f84cf7b61ab16485934717242521cd6">每个层叠上下文和兄弟元素独立</li></ul><ul data-lake-id="6cc600ed1ad555e897be5ea9019c7d39" data-lake-indent="1"><li data-lake-id="e5a80959f0185c7fa92f822409e8446b">当进行层叠变化或渲染的时候,只需要考虑后代元素</li></ul><ul data-lake-id="52fb72c427ee3fae2f2689bc63731057"><li data-lake-id="8a35c7de0f4ec8836f851840a56922fd">每个层叠上下文是自成体系的,当元素发生层叠的时候,整个元素被认为是在父层叠上下文的层叠顺序中</li></ul><h3 data-lake-id="cf12644f73c6e27a487587c58ea914d7" id="WxMnT" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="96a422a71a306b20d4d0453da4ee7f75" id="V0s82" style="padding-left: 9px;">层叠上下文的创建(3类)</h3><div data-lake-id="97327305b6d35c5da760d9b4d3d61266" style="padding-left: 9px;"><br /></div><div data-lake-id="09d6db27c5575f6923dbcf936f33e1eb">由一些CSS属性创建</div><ol data-lake-id="dc155018b282860e233798b5d5f9750d"><li data-lake-id="f99da208df9865f9ca66afda10cd33e6"><strong>天生派</strong></li></ol><ul data-lake-id="9d88a08671b980e56f7a2759fc509585" data-lake-indent="1"><li data-lake-id="78f483cab203aefb7f8cba0935b86c4a" style="padding-left: 6px;"><strong>页面根元素天生具有层叠上下文</strong></li><li data-lake-id="fa292bc9ce2af5117833a9b91c588d3a" style="padding-left: 6px;">根层叠上下文</li></ul><ol data-lake-id="524cb02f396198365ecdeb5237aaacf7" start="2"><li data-lake-id="2b43f3ccd73c3dca8af8673da5f33740"><strong>正统派</strong></li></ol><ul data-lake-id="e5e5eea11390ad94aaae162472a70263" data-lake-indent="1"><li data-lake-id="6f665f295f681ba85342d733bbb70d29" style="padding-left: 6px;"><code>z-index</code>值为数值的<em>定位元素</em>的传统层叠上下文</li></ul><ol data-lake-id="db06832c49515f106389fb2de0a1b027" start="3"><li data-lake-id="5ea68803a54696c557cee1893f14e3fe"><strong>扩招派</strong></li></ol><ul data-lake-id="674fc55317b2995becd6113144ac9677" data-lake-indent="1"><li data-lake-id="d94bca366b559488713e324e4d326bac" style="padding-left: 6px;">其他CSS3属性</li></ul><h4 data-lake-id="f7f5b46099c5a6be868db2578c30a1de" id="6dKzQ"><br /></h4><h4 data-lake-id="463caf1cf2ada0d48d6668c9368354db" id="JtNsn">根层叠上下文</h4><div data-lake-id="b18443e6fbce20eaf23ba90716b06552"><br /></div><div data-lake-id="b84d08ca5ffbe477e733adf0cb61096d">指的是页面根元素,页面中<strong>所有的元素</strong>一定处于至少一个层叠结界中</div><h4 data-lake-id="6feaca4e8e7a07775620dfbefc828711" id="yINd9"><br /></h4><h4 data-lake-id="292af70acffdcfcf36db6127a9abb2aa" id="zbapv">定位元素与传统层叠上下文</h4><div data-lake-id="d6509a82f8f49ec49259e9d0efd9cf08"><br /></div><div data-lake-id="13956b05fad8dea636b6f67f5976fd35">对于<code>position</code>值为<code>relative/absolute</code>的定位元素,当<code>z-index</code>值不是<code>auto</code>的时候,会创建层叠上下文。</div><h4 data-lake-id="75998365ec887778b355957529cd669c" id="ugk7o"><br /></h4><h4 data-lake-id="6322233a2a46515b275c50e2fc10a658" id="eh15o">CSS3属性(8个)</h4><div data-lake-id="bde8f8e5255c7f421fca2b52bb81d4d9"><br /></div><ol data-lake-id="fed0a6942818c15e00df3b28ea517291"><li data-lake-id="8eb97a4d926d91ecc264cbf44b1f61db" style="padding-left: 6px;">元素为<code>flex</code>布局元素(父元素<code>display:flex|inline-flex</code>),同时<code>z-index</code>值<strong>不是auto</strong> - <strong>flex布局</strong></li><li data-lake-id="30c119dfcfae69fa2491403632c022b3" style="padding-left: 6px;">元素的<code>opactity</code>值不是1 - <span>{透明度|opactity}</span></li><li data-lake-id="aa7f4777a610cde36855e27d12756576" style="padding-left: 6px;">元素的<code>transform</code>值不是<code>none</code> - <span>{转换|transform}</span></li><li data-lake-id="4bde4536df1636547d067960125d9d2b" style="padding-left: 6px;">元素<code>mix-blend-mode</code>值不是<code>normal</code> - <span>{混合模式|mix-blend-mode}</span></li><li data-lake-id="94aa1d56fa7c0456209170edebccab64" style="padding-left: 6px;">元素的<code>filter</code>值不是<code>none</code> - <span>{滤镜|filter}</span></li><li data-lake-id="10a6b8a31656206a654ee16587ba20da" style="padding-left: 6px;">元素的<code>isolation</code>值是<code>isolate</code> - <span>{隔离|isolation}</span></li><li data-lake-id="366409918eb34e2a160999c276e93585" style="padding-left: 6px;">元素的<code>will-change</code>属性值为上面②~⑥的任意一个(如<code>will-change:opacity</code>)</li><li data-lake-id="34e773207893855752cd907460a20e4d" style="padding-left: 6px;">元素的<code>-webkit-overflow-scrolling</code>设为<code>touch</code></li></ol><div data-card-type="block" data-ready-card="hr"></div><h2 data-lake-id="9407d1873cd77b0ea87c85e439988f88" id="h87ta"><br /></h2><h2 data-lake-id="dcc6faeab761d1b7ac42659953ff50c8" id="6XHAi">层叠上下文与层叠顺序</h2><div data-lake-id="7d73c1050f6b3b5ec0e4824876ca9c47"><br /></div><blockquote style="background-color: #FFF9F9;"><div data-lake-id="20112d2c467cce735e60eaff5a2a967d"><span>{层叠顺序|Stacking Order}</span>表示元素发生层叠时有着特定的垂直显示顺序</div></blockquote><div data-lake-id="14e6c7f5b13eceffef2f64240510f6fd">一旦普通元素具有层叠上下文,其<em>层叠顺序</em>就会变高</div><div data-lake-id="47ece04c05d670ab7cffeebdbc96b75f">分两种情况</div><ol data-lake-id="faad9ae68c6a6e2c22b72467ef654bc7"><li data-lake-id="a750bfcd579d51e2ce91f86433a98cab">如果层叠上下文元素<strong>不依赖</strong><code>z-index</code>数值,则其层叠顺序是<code>z-index:auto</code></li></ol><ul data-lake-id="2aaf4ec896d8fc4e70e65f9e7e253dc9" data-lake-indent="1"><li data-lake-id="03bc8176dc983825cd1b28d64a85d80d" style="padding-left: 6px;">可看成<code>z-index:0</code></li></ul><ol data-lake-id="2816c5a2d4d6882567e5398eed0c9bf2" start="2"><li data-lake-id="51984ea96aab468053d549552fda75f0" style="padding-left: 6px;">如果层叠上下文元素<strong>依赖</strong><code>z-index</code>数值,则其层叠顺序由<code>z-index</code>值决定</li></ol><div data-lake-id="a9e115cfb74b7dcf77db0a47d501cf38"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_2cee325f9b2141e2b7510454b4960923.png%22%2C%22originWidth%22%3A1212%2C%22originHeight%22%3A744%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A1212%2C%22height%22%3A744%7D"></span></div><div data-lake-id="a5c8a5dba2d5f5fb887ffc3073445e5e">定位元素会层叠在普通元素的上面?</div><div data-lake-id="bf7fc369ded3bd49199211516c87e260">根本原因就是:元素一旦成为<strong>定位元素</strong>,其<code>z-index</code>就会自动生效,其<code>z-index</code>就是默认的<code>auto</code>.</div><div data-lake-id="c3b2dfd16f459cb9d4d700de39eb5b2f">不支持<code>z-index</code>的层叠上下文元素天然是<code>z-index:auto</code>级别,<strong>层叠上下文元素</strong>和<strong>定位元素</strong>是一个层叠顺序的。</div><div data-card-type="block" data-ready-card="hr"></div><h2 data-lake-id="c382c618bc8fe8a6f6c9a140ccdcb033" id="qPqqw"><br /></h2><h2 data-lake-id="d7b0d79812edd4b7e4625d76c0c67d34" id="vIaA9">z-index</h2><div data-lake-id="140ae1f1978233a63d0f5f86a2ff3674"><br /></div><h3 data-lake-id="b77f7bec74b77f173efe402bce3439bb" id="j6Kdg" style="padding-left: 9px;">z-index负值</h3><div data-lake-id="0023de1f85aaf904e944c509dfc72df9" style="padding-left: 9px;"><br /></div><div data-lake-id="ba42768876081cab11f2eb5349bd2afe"><strong><code>z-index</code>是支持负值的</strong>,<code>z-index</code>负值渲染的过程就是一个<strong>寻找第一个层叠上下文元素的过程</strong>,然后层叠顺序止步于这个层叠上下文元素</div><div data-lake-id="6375807a29492a92528f53147654f69f">要实现<strong>父元素覆盖子元素</strong>--正确的解法是把子元素的<code>z-index</code>设置为负数,这样父元素是一个块级元素,<code>z-index<0</code> 的子元素会在块级元素之下,就可以实现我们想要的效果。</div><div data-lake-id="7f63dac0dcf52ce206ca2e7fa091d56e"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_c88d48b0ade642ffa432e40a05028780.png%22%2C%22originWidth%22%3A861%2C%22originHeight%22%3A586%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A861%2C%22height%22%3A586%7D"></span></div><h3 data-lake-id="9068b8cd8eb2fb9a7291fddc47d006be" id="0dkv8" style="padding-left: 9px;"><br /></h3><h3 id="9QJr7" data-lake-id="bf21fe4f7b0ca60bd2f204772a23bd7a" style="padding-left: 9px;"><br /></h3><h3 id="TJcKr" data-lake-id="64acd5ed90487ce57e657d3f77f4d48b" style="padding-left: 9px;">z-index使用准则</h3><div data-lake-id="081ea7c59dfba0d61b272fbc44fa4ebb" style="padding-left: 9px;"><br /></div><div data-lake-id="656e4186121376cf846aea705aac724d">对于非浮层元素,避免设置<code>z-index</code>值,<code>z-index</code>值没有任何道理需要超过2</div><div data-lake-id="d0c29958c2d77301f2f8ad738987cb5c">定位元素一旦设置了<code>z-index</code>值,就从普通定位元素变成了层叠上下文元素,相互间的层叠顺序就发生了根本变化,很容易出现设置了巨大的<code>z-index</code>值无法覆盖其他元素的问题</div><div data-card-type="block" data-ready-card="hr"></div><h1 data-lake-id="740eddbd8ccb7ed7d5927c2f69e42119" id="9Juhz" style="text-align: center;"><span><br /></span></h1><h1 data-lake-id="bc96d94f2404cb2a3ab121598d444e00" id="u47Zu" style="text-align: center;"><span>{块级格式化上下文|Block Formatting Context}</span></h1><div data-lake-id="68658c863c84d2d09c04018a6caca40f" style="text-align: center;"><span><br /></span></div><div data-lake-id="8458c2145bdfbd0fc19b318f27b9aa8e"><span>{块级格式化上下文|Block Formatting Context}</span>(<strong>BFC</strong>),它是页面中的一块<em>渲染区域</em>,并且有一套属于自己的渲染规则:</div><ol data-lake-id="20dfb950241fde131045a6d4b65d549c"><li data-lake-id="8ddb5828fd757cf6ffacf58d91865826" style="padding-left: 6px;">内部的盒子会在<strong>垂直方向</strong>一个接一个的放置</li><li data-lake-id="aa55c232a0d8b865b9ad679c38c1e9aa" style="padding-left: 6px;">对于<strong>同一个</strong>BFC的俩个相邻的盒子的<strong>margin会发生重叠,与方向无关</strong>。</li><li data-lake-id="ae9a84ca1eafe69b108983f836c61cd8" style="padding-left: 6px;"><strong>每个元素的左外边距与包含块的左边界相接触</strong>(页面布局方向从左到右),即使浮动元素也是如此</li><li data-lake-id="74848870d4c2b4992cc976c1a0cb853a" style="padding-left: 6px;">BFC的区域不会与float的元素区域重叠</li><li data-lake-id="f5baa70c37f24fb69fd1ed39697912c8" style="padding-left: 6px;"><strong>计算BFC的高度时,浮动子元素也参与计算</strong></li><li data-lake-id="1d7b44843a6c7b3a352d8edf0f03589d" style="padding-left: 6px;">BFC就是页面上的一个<strong>隔离的独立容器</strong>,容器里面的子元素不会影响到外面的元素,反之亦然</li></ol><h2 data-lake-id="9cb7cac81c327c155a33bc830f8ee11b" id="3TfND"><br /></h2><h2 data-lake-id="8f5629f376742582c90c14f1538cd16d" id="uH28Q">触发条件 (5个)</h2><div data-lake-id="b6e4b068505fd6bd98dd5588fcb898ab"><br /></div><ol data-lake-id="e5b760c869e1bfb7dbdb3659d47599cd"><li data-lake-id="6ff2262ede31b249cdc79c84b2b54e8e" style="padding-left: 6px;"><strong>根元素</strong>,即HTML元素</li><li data-lake-id="603c544d7e515d483fefacad7ca4ff79" style="padding-left: 6px;"><strong>浮动元素</strong>:<code>float</code>值为<code>left、right</code></li><li data-lake-id="9809e742cd4ea05742d4a4a37944ff24" style="padding-left: 6px;"><code>overflow</code>值不为 <code>visible</code>,为 <code>auto</code>、<code>scroll</code>、<code>hidden</code></li><li data-lake-id="b395f5cfb7f380b36cc1e193c237baf7" style="padding-left: 6px;"><code>display</code>的值为<code>inline-block、table、inline-table、flex、inline-flex、grid、inline-grid</code></li><li data-lake-id="b3a28710b4d3628d87d5b32d0163e601" style="padding-left: 6px;"><code>position</code> 的值为<code>absolute</code>或<code>fixed</code></li></ol><h2 data-lake-id="64ba199177b4726edbd44a829779438f" id="Mmx7V"><br /></h2><h2 data-lake-id="403d48bc6b8f501b5352e1df77d003da" id="RTffg">应用场景</h2><div data-lake-id="22cdaaa786a6adf3d2e67ec4fa4b402a"><br /></div><ol data-lake-id="a8a6dd2271c8300132147d9c682d071f"><li data-lake-id="003d85eda8d15f4c992949a39444b2b7">防止<code>margin</code>重叠</li></ol><ul data-lake-id="1b9918e999bd9beb50afd1eb53af8feb" data-lake-indent="1"><li data-lake-id="8e51d37a0b8befe72a1234d94e642f4b" style="padding-left: 6px;">将位于同一个BFC的元素,分割到不同的BFC中</li></ul><ol data-lake-id="635e82f6a6212849d4ea9fc855d378b8" start="2"><li data-lake-id="50f3fe39f4964ffc4b5a08ad823b9d6c">高度塌陷 ---<strong>计算BFC的高度时,浮动子元素也参与计算</strong></li></ol><ul data-lake-id="3dbaf1bc8a3c39c348fdf5b3b67201df" data-lake-indent="1"><li data-lake-id="2f313faa08b7197a84cdd8e58bbec4dd" style="padding-left: 6px;">子元素浮动</li><li data-lake-id="85f95b8e8c5fbed4df7faa623e18d3ef" style="padding-left: 6px;">父元素 <code>overflow: hidden;</code>构建BFC</li></ul><ol data-lake-id="c7eebf7bda6dfdda1f5789f586bd8478" start="3"><li data-lake-id="4c19d2455e3214163d44bfe6b0b19ceb">多栏自适应 --- BFC的区域不会与float的元素区域重叠</li></ol><ul data-lake-id="42277c4ef72389215b3bc89c3f38ad1f" data-lake-indent="1"><li data-lake-id="ba781db76880d771b600d1ce8b913097" style="padding-left: 6px;"><code>aside</code> 左浮动</li><li data-lake-id="14ab719f1729b95ed1ca062594b70824"><code>main</code>--></li></ul><ul data-lake-id="4a3144aac66ee32a77af94f94a29afd0" data-lake-indent="2"><li data-lake-id="7a267d6a1eaf29aa4f39bc5a870f3e63" style="padding-left: 6px;"><code>margin-left:aside-width</code></li><li data-lake-id="53d18982f777a2bfb3cc33a09b3bfd87" style="padding-left: 6px;"><code>overflow: hidden</code>构建BFC</li></ul><div data-card-type="block" data-ready-card="hr"></div><h1 data-lake-id="08d394fa177043e39acc28d1cf3da1cc" id="IVQb8" style="text-align: center;"><br /></h1><h1 data-lake-id="750b5db9c1afcd2feda7d6de1a0d3a4f" id="xnBEG" style="text-align: center;">元素居中</h1><div data-lake-id="ccda7a752f0438d5e8f599ddc275492a" style="text-align: center;"><br /></div><h2 data-lake-id="f8db7462372e423216b656d63e2a5bcc" id="Q6qmb">水平居中</h2><div data-lake-id="7477082f5504f843edbfc65c363fae4c"><br /></div><ol data-lake-id="63b2dd98dd10cf92b4d597f8bf1ffcb8"><li data-lake-id="ba6c4055013f534b1efccfa67e68965f">行内元素-水平居中</li></ol><ul data-lake-id="c03fcc3c5d48380d589bb19d7273a152" data-lake-indent="1"><li data-lake-id="13695a744a79384e2d7496c4ec94afe8" style="padding-left: 6px;"><code>text-align:center</code></li></ul><ol data-lake-id="09da00348f0514e06c0891e883e05276" start="2"><li data-lake-id="cb71d1a69d715d001b41aa5370437de2">块级元素-水平居中</li></ol><ul data-lake-id="9754b33db6825b3eeed36d812d85a391" data-lake-indent="1"><li data-lake-id="31c9eb0f88763ee6157d62f61e1fc59d"><strong>固定宽度</strong>的块级元素-水平居中</li></ul><ul data-lake-id="f0025a6c8e204a7aff73db153a8cb820" data-lake-indent="2"><li data-lake-id="778ffc520a29acb8b400ae4dc738edb0" style="padding-left: 6px;"><code>margin:0 auto</code></li></ul><ul data-lake-id="9b971fbfcc241264d27e33d32df838dc" data-lake-indent="1"><li data-lake-id="5e139a18134a23f606684f9a6679cde6"><strong>多个块级元素</strong>-水平居中</li></ul><ul data-lake-id="1cc3d9b11601b68480c4ecd8f5017561" data-lake-indent="2"><li data-lake-id="041bbd1b2896bda898f2b4d3de42028c" style="padding-left: 6px;">块级元素<code>inline-block</code>化</li><li data-lake-id="07ada4e76d06d7ff62b08491ed826723" style="padding-left: 6px;">利用<code>flexbox</code></li></ul><h3 data-lake-id="40fe56959a7bfad3c89ac58413b3d8f5" id="qDH0S" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="9133071a9bce2fdd39dea5fb02b91315" id="M0Se3" style="padding-left: 9px;">行内元素-水平居中</h3><div data-lake-id="ae1e86565ae4f67c8227cd96599ccaca" style="padding-left: 9px;"><br /></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%2F%2F%20%E8%A1%8C%E5%86%85%E5%85%83%E7%B4%A0-%E6%B0%B4%E5%B9%B3%E5%B1%85%E4%B8%AD%5Cn.center-inline%20%7B%5Cn%20%20text-align%3A%20center%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22rFLPH%22%7D"></div><h3 data-lake-id="27f76830ed905c39a2045b5a4da4259b" id="4YjAd" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="9ee55f713b7ecf9e7f823d71778aaafd" id="5Fnrj" style="padding-left: 9px;">块级元素-水平居中</h3><div data-lake-id="b178e72623570f6570520b00d174bd96" style="padding-left: 9px;"><br /></div><h4 data-lake-id="27805ff8ab908bff41579b7b9d2b8e64" id="K3wDx">固定宽度的块级元素-水平居中</h4><div data-lake-id="236a7b6c5d9f1237f180d8863c14852d"><br /></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%2F%2F%20%E5%9B%BA%E5%AE%9A%E5%AE%BD%E5%BA%A6%E7%9A%84%E5%9D%97%E7%BA%A7%E5%85%83%E7%B4%A0-%E6%B0%B4%E5%B9%B3%E5%B1%85%E4%B8%AD%5Cn.center-block-fixed-width%20%7B%5Cn%20%20margin%3A%200%20auto%3B%5Cn%20%20width%3A78px%3B%20%2F%2F%20%E4%B8%8D%E8%83%BD%E7%BC%BA%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22oOGmM%22%7D"></div><h4 data-lake-id="ce738d0a971e2ccf5964a461b63f3d9a" id="AZFqa"><br /></h4><h4 data-lake-id="76dff5c468b572a94aeb2c29f4780beb" id="LXcYW">多个块级元素-水平居中</h4><div data-lake-id="bdf668f519950dc931913eaf5cb995dd"><br /></div><div data-lake-id="fe1ec8c08119ceebe2e629740c80c715"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_86f1cce7a4d8456498ed72fe74e2a4e3.png%22%2C%22originWidth%22%3A1012%2C%22originHeight%22%3A158%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A1012%2C%22height%22%3A158%7D"></span></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%2F%2F%20xx%20%E4%BC%9A%E8%A2%AB%E5%90%8E%E7%BB%AD%E7%9A%84%E7%89%B9%E5%AE%9A%E7%9A%84%E7%B1%BB%E5%90%8D%E6%9B%BF%E6%8D%A2%5Cn%3Cmain%20class%3D%5C%22xx-center%5C%22%3E%5Cn%20%20%3Cdiv%3E%5Cn%20%20%20%20%E5%9D%971%5Cn%20%20%3C%2Fdiv%3E%5Cn%20%20%3Cdiv%3E%5Cn%20%20%20%20%E5%9D%972%5Cn%20%20%3C%2Fdiv%3E%5Cn%20%20%3Cdiv%3E%5Cn%20%20%20%20%20%E5%9D%973%5Cn%20%20%3C%2Fdiv%3E%5Cn%3C%2Fmain%3E%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%222pGhl%22%7D"></div><h5 data-lake-id="4a30a274a51904e16d1777482ed7b08b" id="zE828">inline-block</h5><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%2F%2F%20%E7%88%B6%E5%85%83%E7%B4%A0%20%E8%AE%BE%E7%BD%AE%E6%B0%B4%E5%B9%B3%E5%B1%85%E4%B8%AD%5Cn.inline-block-center%20%7B%5Cn%20%20text-align%3A%20center%3B%5Cn%7D%5Cn%2F%2F%20%E5%9D%97%E7%BA%A7%E5%85%83%E7%B4%A0%20%60inline-block%60%E5%8C%96%5Cn.inline-block-center%20div%20%7B%5Cn%20%20display%3A%20inline-block%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22BbjyS%22%7D"></div><h5 data-lake-id="df467f2912f0aafb3db7ff301dd5d71b" id="xTwlN">flexbox</h5><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.flex-center%20%7B%5Cn%20%20display%3A%20flex%3B%5Cn%20%20justify-content%3A%20center%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22OaKam%22%7D"></div><div data-card-type="block" data-ready-card="hr"></div><h2 data-lake-id="1ce6974fe0584f6cc2a67abbedd253fc" id="EaxX9"><br /></h2><h2 data-lake-id="945fb3c88e6c0be8b646219ec8ca059f" id="ZOGh2">垂直居中</h2><div data-lake-id="855acf8e015f8210eb07cfae1c6e8a29"><br /></div><ol data-lake-id="297ebcf5d5f1a0095c635e16066d7109"><li data-lake-id="908234c6a380dcd95e43136b92d6c283">行内元素-垂直居中</li></ol><ul data-lake-id="eb175ef5f2726f8b69587788caffc788" data-lake-indent="1"><li data-lake-id="c1bf546d0de8604c1c3d34cedbfc9c9d">单行</li></ul><ol data-lake-id="ab901a636ae78a37cf050c839dd51a42" data-lake-indent="2"><li data-lake-id="f17b02bcf088ff7b1ace118ed62c628b" style="padding-left: 6px;">设置上下<code>padding:xx</code></li><li data-lake-id="38730582c7d3430d8003ae3b0ead95d1" style="padding-left: 6px;"><code>line-height:xx</code></li></ol><ul data-lake-id="a3a2c65953e4205d530f28bb4f4a4568" data-lake-indent="1"><li data-lake-id="56241dab6a93294492e92b155ce57380">多行</li></ul><ol data-lake-id="659009996a51a065e8b8e42209bb11a8" data-lake-indent="2"><li data-lake-id="674df9eb6fefc509371695a9bd980528" style="padding-left: 6px;"><code>table</code>布局</li><li data-lake-id="7745616cacf9c697fc3adca2612178c6" style="padding-left: 6px;"><code>flexbox</code></li></ol><ol data-lake-id="9c13d0e41a4791c7cfba7aaa5d1f92be" start="2"><li data-lake-id="11ccd99490187fe7cf9048d09a9bbced">块级元素-垂直居中</li></ol><ul data-lake-id="d3b99b8a34e1c22f1971f266f5204be8" data-lake-indent="1"><li data-lake-id="c141fece9ab0de1bc5806c2508c05ca9">元素定高</li></ul><ul data-lake-id="f99c35207efc9d456bd13b96585712fd" data-lake-indent="2"><li data-lake-id="bc77d5d9763b5bcdf1370b734e955a15" style="padding-left: 6px;"><strong>子绝父相</strong> + 子元素<code>top:50%</code> + 子元素负<code>margin</code></li></ul><ul data-lake-id="f319c19f4ce1421a7c94c4002074eac3" data-lake-indent="1"><li data-lake-id="7817e3f0b0db2edbf4c5247a4083551e">元素高度不确定</li></ul><ul data-lake-id="b04ddcc021e9733b1aa5dffc35354cdc" data-lake-indent="2"><li data-lake-id="40e1615ba846a9d43a09352f0268bab3" style="padding-left: 6px;"><strong>子绝父相</strong> + 子元素<code>top:50%</code> + <code>transform: translateY(-50%)</code></li></ul><ul data-lake-id="5cc4c09bdd5778461b6ddc9d67a75bb8" data-lake-indent="1"><li data-lake-id="a51650dcdcd32034b6959187ad70884b"><code>flexbox</code></li></ul><ul data-lake-id="f22c7d15cb5547fa78884550727d3c76" data-lake-indent="2"><li data-lake-id="20852c1b8547e0574b519b8711bdb6dd" style="padding-left: 6px;"><code>flex-direction: column</code>;</li><li data-lake-id="c27a8fb4d4729bbdb2164d9699c30c2e" style="padding-left: 6px;"><code>justify-content: center</code>;</li></ul><h3 data-lake-id="cece0551e6bbe9d38a9e36e3a2282232" id="XO9R6" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="bd58be607ecd3c6da62248f1351b8ede" id="dIeP8" style="padding-left: 9px;">行内元素-垂直居中</h3><div data-lake-id="c7230e81976803150464546614a38c90" style="padding-left: 9px;"><br /></div><h4 data-lake-id="033e4836a008340fbe4f8d6c4fc610b7" id="uEDbR">单行</h4><div data-lake-id="57a637b35ce24ae516d80ca5e25a1ff1"><br /></div><h5 data-lake-id="efd8efd5e7c7a5d660a44e320b9d5f1f" id="jUeWy">设置padding</h5><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.center-text-vertical%20%7B%5Cn%20%20padding-top%3A%2030px%3B%5Cn%20%20padding-bottom%3A%2030px%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22poDyN%22%7D"></div><h5 data-lake-id="7a134b315ec59169816a157987ccffa5" id="chc7q">设置line-height</h5><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.center-text-vertical-trick%20%7B%5Cn%20%20line-height%3A%20100px%3B%5Cn%20%20white-space%3A%20nowrap%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22aBhvP%22%7D"></div><div data-card-type="block" data-ready-card="hr"></div><h4 data-lake-id="65a139492128456dd063473704469529" id="FXmFN"><br /></h4><h4 data-lake-id="6559235caf1375c44789c1dc1dacc847" id="bD2nZ">多行</h4><div data-lake-id="5fa32bba3efef7f7e68cb7eba383e878"><br /></div><div data-lake-id="9e1bce10874c349b42e511ff16e59646">有如下的HTML结构,我们想实现<code><div></code>元素内文本,在垂直方向居中显示</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%3Cdiv%20class%3D%5C%22xxx%5C%22%3E%5Cn%20%20%3Cp%3E%E6%88%91%E6%98%AF%E4%B8%80%E4%B8%AA%E5%A4%9A%E8%A1%8C%E6%96%87%E6%9C%AC%E4%BF%A1%E6%81%AF%20bala%20bala%20%3C%2Fp%3E%5Cn%3C%2Fdiv%3E%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22wikFb%22%7D"></div><h5 data-lake-id="48a4b5374f0e777e310960f71b41ae27" id="mamL5">利用display:table</h5><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.center-table%20%7B%5Cn%20%20display%3A%20table%3B%5Cn%7D%5Cn.center-table%20p%20%7B%5Cn%20%20display%3A%20table-cell%3B%5Cn%20%20%2F%2F%20%E6%89%8B%E5%8A%A8%E6%8C%87%E5%AE%9A%20%E5%9E%82%E7%9B%B4%E6%96%B9%E5%90%91%E5%B1%85%E4%B8%AD%E6%98%BE%E7%A4%BA%5Cn%20%20vertical-align%3A%20middle%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22YMF0K%22%7D"></div><h5 data-lake-id="169ab9c51146ef958106cacdbd90d8cc" id="JeZ3B">flexbox</h5><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.flex-center%20%7B%5Cn%20%20display%3A%20flex%3B%5Cn%20%20flex-direction%3A%20column%3B%5Cn%20%20justify-content%3A%20center%3B%5Cn%20%20height%3A200px%3B%20%20%2F%2F%E8%BF%99%E9%87%8C%E4%B8%8D%E8%83%BD%E7%BC%BA%E5%B0%91%5Cn%7D%20%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22ZSTi5%22%7D"></div><div data-card-type="block" data-ready-card="hr"></div><h3 data-lake-id="1bf64e1d9efa735d26828134fac408d3" id="FGXhU" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="b5ceaf81f8667a91c28d6dd630a50f18" id="k37eO" style="padding-left: 9px;">块级元素-垂直居中</h3><div data-lake-id="5dc372ead7dcd3a29a3ef3743c80ad1c" style="padding-left: 9px;"><br /></div><h4 data-lake-id="496ccbf0000d656c6d24fe91ffca5088" id="RqrA3">元素定高</h4><div data-lake-id="09bb58f05af75e4e640ac3cb98ca90a7"><br /></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.parent%20%7B%5Cn%20%20position%3A%20relative%3B%5Cn%7D%5Cn.child%20%7B%5Cn%20%20position%3A%20absolute%3B%5Cn%20%20top%3A%2050%25%3B%5Cn%20%20height%3A%20100px%3B%5Cn%20%20margin-top%3A%20-70px%3B%20%5Cn%20%20padding%3A20px%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22PW4Nh%22%7D"></div><h4 data-lake-id="e0ab77aa5bab2b24b667ad887c5a5ecd" id="IAfSR"><br /></h4><h4 data-lake-id="544a645a24dfabd9ec929b809c224423" id="3IqMB">元素高度不确定</h4><div data-lake-id="596db0bdd4ff02548c3397797280d589"><br /></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.parent%20%7B%5Cn%20%20position%3A%20relative%3B%5Cn%7D%5Cn.child%20%7B%5Cn%20%20position%3A%20absolute%3B%5Cn%20%20top%3A%2050%25%3B%5Cn%20%20transform%3A%20translateY(-50%25)%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22RHiTK%22%7D"></div><h4 data-lake-id="895056875c411958c207b7b6f68b0c5d" id="wQmwJ"><br /></h4><h4 data-lake-id="bb47174e93c68e1781721840782e48a7" id="jpG07">flex</h4><div data-lake-id="f82244ed0b2204acf98b8617bb32daf8"><br /></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.parent%20%7B%5Cn%20%20display%3A%20flex%3B%5Cn%20%20flex-direction%3A%20column%3B%5Cn%20%20justify-content%3A%20center%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22frcl1%22%7D"></div><div data-card-type="block" data-ready-card="hr"></div><h2 data-lake-id="ce91182937bae64c14625cfab351c54b" id="TjrBU"><br /></h2><h2 data-lake-id="5c2ad21097da0756acd8796cfd7d70ab" id="CuEuy">水平垂直居中</h2><div data-lake-id="e77a4eec08d6b6d18d014a017b6a0e89"><br /></div><ol data-lake-id="b8349818012f887466b331ed4d71e862"><li data-lake-id="2efbd3b9fa659db9a0f62b89756bcc46">宽&高固定</li></ol><ol data-lake-id="0a71e11cfa8903403181ff90f03d01dc" data-lake-indent="1"><li data-lake-id="f7ac17dc32316c1504467fb9cef193c1" style="padding-left: 6px;"><code>absolute</code> + 负 <code>margin</code></li><li data-lake-id="aecfc6ee31ab3c5a1dd833211758b9e1" style="padding-left: 6px;"><code>absolute</code> + <code>margin auto</code></li><li data-lake-id="852535c4c3c2f2c5226216c6c607e9ab" style="padding-left: 6px;"><code>absolute</code> + <code>calc</code></li></ol><ol data-lake-id="a1530bee18465961c0727ae720fd208e" start="2"><li data-lake-id="9c93cc9197967f0a9e5aa6cc17720d74">宽&高不固定</li></ol><ol data-lake-id="6431ce72d8992cf4ca6ba3c2d183e6a4" data-lake-indent="1"><li data-lake-id="f60f12c1d4462ebe91e0b7fd7f2469e9" style="padding-left: 6px;"><code>absolute</code> + <code>transform: translate(-50%, -50%);</code></li><li data-lake-id="ba563f3bde84480842f95f62946d0333" style="padding-left: 6px;">flex布局</li><li data-lake-id="9a151860b19c56d585563650800d2031" style="padding-left: 6px;">grid 布局</li></ol><h3 data-lake-id="b8a697bde2309ee371dce36ef1f4284a" id="Jm5zi" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="a4a9ae1eed027ea4a4f308bded397120" id="t5Xsk" style="padding-left: 9px;">宽&高固定</h3><div data-lake-id="1ab5962e822956131a827d1e5b447aff" style="padding-left: 9px;"><br /></div><h4 data-lake-id="e82ce3e1004a4a5c546a8660553c12cf" id="iOlaa"><code>absolute</code> + 负 <code>margin</code></h4><div data-lake-id="c616a8d26b1bfd52d3fbdef3306acad6"><br /></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.parent%20%7B%5Cn%20%20position%3A%20relative%3B%5Cn%7D%5Cn.child%20%7B%5Cn%20%20width%3A%20300px%3B%5Cn%20%20height%3A%20100px%3B%5Cn%20%20padding%3A%2020px%3B%5Cn%20%20position%3A%20absolute%3B%5Cn%20%20top%3A%2050%25%3B%5Cn%20%20left%3A%2050%25%3B%5Cn%20%20margin%3A%20-70px%200%200%20-170px%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22Yfiog%22%7D"></div><div data-lake-id="b7e084eeb7d59fed1dd12e3349797655"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_49a078c5140243c38990360c9f9c10bd.png%22%2C%22originWidth%22%3A743%2C%22originHeight%22%3A558%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A743%2C%22height%22%3A558%7D"></span></div><ul data-lake-id="9a9720fbc07637c9d847c97ec8082177"><li data-lake-id="3b834abd5755540e1e5972f5fd168254">初始位置为方块1的位置</li><li data-lake-id="12c8d516092968a8ceecd5458b516068">当设置<code>left、top</code>为50%的时候,内部子元素为方块2的位置</li><li data-lake-id="962b651154cadf1e4d7939c385a77673">设置<code>margin</code>为负数时,使内部子元素到方块3的位置,即中间位置</li></ul><h4 data-lake-id="377a40208aff3d4472812ad07d67fcd2" id="2THdW"><code><br /></code></h4><h4 data-lake-id="f89d9639a4767d9a6936456152c2ea82" id="Ok0q1"><code>absolute</code> + <code>margin auto</code></h4><div data-lake-id="cf489d85221dcea4d2adfba43a3a3ad3"><br /></div><div data-lake-id="7832998fe6bdb67b31123accad2ab24a"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_5fb7e1acf74049e0ba5afea77dc399ae.png%22%2C%22originWidth%22%3A884%2C%22originHeight%22%3A532%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A884%2C%22height%22%3A532%7D"></span></div><h4 data-lake-id="f97a7b4a45546b14d1f238f87b04f1cd" id="eEYDv"><code><br /></code></h4><h4 data-lake-id="549a029af05100018dbda9e12bab8c42" id="8z7zi"><code>absolute</code> + <code>calc</code></h4><div data-lake-id="530f500f3934ea3fcae411da7116fa7c"><br /></div><div data-lake-id="bcfedac1547b77671b8766d9dc84f1a3"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_05978f769faa463882a149191124f62d.png%22%2C%22originWidth%22%3A904%2C%22originHeight%22%3A422%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A904%2C%22height%22%3A422%7D"></span></div><div data-card-type="block" data-ready-card="hr"></div><h3 data-lake-id="6809902c6a326dd25c181daf03e2b965" id="NU15q" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="5611bff3503acc49d866de49e025c89e" id="WmMpZ" style="padding-left: 9px;">宽&高不固定</h3><div data-lake-id="213e106dd21a32fe4ac719b2e6b10f45" style="padding-left: 9px;"><br /></div><h4 data-lake-id="28a9f1aa9c72fb7fa4f7a161f32f8d26" id="ytxE1"><code>absolute</code> + <code>transform: translate(-50%, -50%);</code></h4><div data-lake-id="875c54d6be2d38f97a4be7056b4163e9"><br /></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.parent%20%7B%5Cn%20%20position%3A%20relative%3B%5Cn%7D%5Cn.child%20%7B%5Cn%20%20position%3A%20absolute%3B%5Cn%20%20top%3A%2050%25%3B%5Cn%20%20left%3A%2050%25%3B%5Cn%20%20transform%3A%20translate(-50%25%2C%20-50%25)%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22PcWAo%22%7D"></div><h4 data-lake-id="d2cc86a6f6f9a38d3e7b77ead889772e" id="FZ70v"><br /></h4><h4 data-lake-id="269392642e800e96a2675d7a4959d903" id="zaX12">flex布局</h4><div data-lake-id="59e23de652cb16a7f8c22b93b00f8501"><br /></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.parent%20%7B%5Cn%20%20display%3A%20flex%3B%5Cn%20%20justify-content%3A%20center%3B%5Cn%20%20align-items%3A%20center%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%2266tU7%22%7D"></div><h4 data-lake-id="8c765877d71c50f9bac955eb9d868fff" id="Pjdwo"><br /></h4><h4 data-lake-id="9bf16e7cda64a3ffff449e9454316c10" id="biAPA">grid布局</h4><div data-lake-id="49c22fa3dc04ccd2fe7a0257a2c70f09"><br /></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.parent%20%7B%5Cn%20%20display%3Agrid%3B%5Cn%7D%5Cn.parent%20.child%7B%5Cn%20%20margin%3Aauto%3B%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22gnXoH%22%7D"></div><div data-card-type="block" data-ready-card="hr"></div><h1 data-lake-id="60fc99cf513029c5442a699eacdfcaba" id="4BMjb" style="text-align: center;"><br /></h1><h1 data-lake-id="a77d6885f64387e3972f43cc138e65df" id="unM5V" style="text-align: center;">flex 布局</h1><div data-lake-id="b620e6fb11fcb9d351ad7350a3a258bf" style="text-align: center;"><br /></div><div data-lake-id="560dd0f745b86d308aaaa2d45c6f6c42">采用<code>flex</code>布局的元素,称为flex<span>{容器|Container}</span></div><div data-lake-id="a7a017100fdd869a3e6f4cf860e67e1a">它的所有子元素自动成为容器成员,称为flex<span>{项目|Item}</span></div><div data-lake-id="2ce65b2d893c004c0eb04d1456ab7635"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_89bc73a9d95d4af181edfc8268309eeb.png%22%2C%22originWidth%22%3A563%2C%22originHeight%22%3A333%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A563%2C%22height%22%3A333%7D"></span></div><div data-lake-id="c80c8225af7df6a30da2d3775f09c1c3">容器默认存在两根轴:水平的<span>{主轴|main axis}</span>和垂直的<span>{交叉轴|cross axis}</span></div><h2 data-lake-id="53f3fc33b286fff9a58e794d28e18fbf" id="TEdMu"><br /></h2><h2 data-lake-id="df1919a370c417909c7db6341550473e" id="xMBGH">容器的属性 (6个)</h2><div data-lake-id="d78c1e9c385f638c5c948ded28f080e8"><br /></div><ol data-lake-id="bf93b0855cb7b1b581e6873dad35909a"><li data-lake-id="81d08ca8ae99cedef146990594d3009d" style="padding-left: 6px;"><code>flex-direction</code></li><li data-lake-id="2ac0d8ea8fcc9f7d709ebd8919031a67" style="padding-left: 6px;"><code>flex-wrap</code></li><li data-lake-id="da3ce92e7ffa9208d32b08ebcd84465e" style="padding-left: 6px;"><code>flex-flow</code></li><li data-lake-id="fe8085574f412f5ac7e494bf4380aefa" style="padding-left: 6px;"><code>justify-content</code></li><li data-lake-id="129a1d9cfb0e81f8efcba87059b75924" style="padding-left: 6px;"><code>align-items</code></li><li data-lake-id="6b4ff663c00be96db10dc565926167e5" style="padding-left: 6px;"><code>align-content</code></li></ol><h3 data-lake-id="e2cf4489c81740b52d75c607e67c1e31" id="1i9fF" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="4f146ca296339528439dfa4e519b2544" id="wvWFp" style="padding-left: 9px;">flex-direction属性</h3><div data-lake-id="e982b819a6079340c2c90fe311e0ac3f" style="padding-left: 9px;"><br /></div><div data-lake-id="b0305746890931e8270d6f8a11794c82"><code>flex-direction</code>属性决定主轴的方向(即项目的排列方向)。</div><ol data-lake-id="b573a706fbec86c6466f3892c906f1fe"><li data-lake-id="928f8ae44bfbfd4da84d496246cdda6c" style="padding-left: 6px;"><code>row</code>(<strong>默认值</strong>):主轴为水平方向,起点在左端。</li><li data-lake-id="a2dec8c07564cd77731bbf2f0c3acceb" style="padding-left: 6px;"><code>row-reverse</code>:主轴为水平方向,起点在右端。</li><li data-lake-id="30bbcd5e2958da848ed9b9e19eea98c6" style="padding-left: 6px;"><code>column</code>:主轴为垂直方向,起点在上沿。</li><li data-lake-id="0fa6c8091155813bca49c057401dafa8" style="padding-left: 6px;"><code>column-reverse</code>:主轴为垂直方向,起点在下沿。</li></ol><h3 data-lake-id="25ded54e128000eecfc425c98f9093b8" id="pPbSW" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="23d98fdedc56308e8471ab90f214aa0d" id="jV4po" style="padding-left: 9px;">flex-wrap属性</h3><div data-lake-id="42579bc84720dce55218a4cb9f702140" style="padding-left: 9px;"><br /></div><div data-lake-id="de9bfdb7e0420f21c6f71a9f4262bda4">默认情况下,项目都排在一条线(又称"轴线")上。<code>flex-wrap</code>属性定义,如果一条轴线排不下,如何换行。</div><ol data-lake-id="ec269cfaa3fd3283b0d0e500c66532cb"><li data-lake-id="2c4888ead12da6a03e0f801cd3612ba8"><code>nowrap</code>:(<strong>默认</strong>):不换行。</li></ol><ul data-lake-id="cc300ff3caa48030b3507488f6299f2a" data-lake-indent="1"><li data-lake-id="dee5f2febecfac76491ec4c373fcb9f1" style="padding-left: 6px;"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_6ceafa86ec3144bea1cfc697f8a619f7.png%22%2C%22originWidth%22%3A700%2C%22originHeight%22%3A145%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A700%2C%22height%22%3A145%7D"></span></li></ul><ol data-lake-id="bc50e91ed6821dff8fabdc1610bea1da" start="2"><li data-lake-id="df0a7a0e3b52947326f541ab4cfcbe36"><code>wrap</code>:换行,第一行在上方。</li></ol><ul data-lake-id="308ac8af586ae32e6f339993bc7e5070" data-lake-indent="1"><li data-lake-id="65056c365e77f9907c0bdee92d87155f" style="padding-left: 6px;"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_9fb0b60f94ec4ad49eac4cbdcb6a9569.png%22%2C%22originWidth%22%3A700%2C%22originHeight%22%3A177%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A700%2C%22height%22%3A177%7D"></span></li></ul><ol data-lake-id="75ffe3a83840deb3e6e07d0400668071" start="3"><li data-lake-id="1534d19ef1fc678fc400732507879ed4"><code>wrap-reverse</code>:换行,第一行在下方</li></ol><ul data-lake-id="e1f77cb43c6389b143cf1e56f1554060" data-lake-indent="1"><li data-lake-id="81e548ee27ad063da4a850470d7b5183" style="padding-left: 6px;"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_bff559fc16504a58ad5a669e152132e2.png%22%2C%22originWidth%22%3A700%2C%22originHeight%22%3A177%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A700%2C%22height%22%3A177%7D"></span></li></ul><h3 data-lake-id="a35b12d7b02e6126b36e06f529670a02" id="L9TNw" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="98c814e84d073052031238325057cc8d" id="m6Jv6" style="padding-left: 9px;">flex-flow</h3><div data-lake-id="1ff6024399b8411210f65753ed04c66a" style="padding-left: 9px;"><br /></div><div data-lake-id="02a4a4bda412e1bb2722801495945939"><code>flex-flow</code>属性是<code>flex-direction</code>属性和<code>flex-wrap</code>属性的简写形式,默认值为<code>row nowrap</code>。</div><h3 data-lake-id="d83f39736bb7ef9f49ae014334868e0d" id="IldWk" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="104bd17a30d5d3751085c2f63cba8dda" id="9k6bH" style="padding-left: 9px;">justify-content属性</h3><div data-lake-id="d197dc4dcbfedc7f57b73e9583d5364f" style="padding-left: 9px;"><br /></div><div data-lake-id="19f7294a3256792406ac50f33171cda3"><code>justify-content</code>属性定义了项目在<strong>主轴上的对齐方式</strong>。</div><ol data-lake-id="62a87a6ffc375c34cb0afb530dfface1"><li data-lake-id="d76c898e43856c7f1f7168d0e4e8bd8d" style="padding-left: 6px;"><code>flex-start</code>(<strong>默认值</strong>):左对齐<br /></li><li data-lake-id="798ef459b77d4ffced0f94717cb6d3a3" style="padding-left: 6px;"><code>flex-end</code>:右对齐<br /></li><li data-lake-id="825bd754aed788416aa8e7d567424666" style="padding-left: 6px;"><code>center</code>: 居中<br /></li><li data-lake-id="7f7e502cd05b99ad4c47c17168ac41f5"><code>space-between</code>:<strong>两端对齐</strong>,项目之间的间隔都相等。</li></ol><ul data-lake-id="3032c6c6a1ccc7d9cc7f363074dc4b8d" data-lake-indent="1"><li data-lake-id="84e3b185dc034a3b41076f1a5231eb63" style="padding-left: 6px;"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_a12045726a864209ae8bfa6f50c5350c.png%22%2C%22originWidth%22%3A1276%2C%22originHeight%22%3A246%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A1276%2C%22height%22%3A246%7D"></span></li></ul><ol data-lake-id="3d0c69f3bbcac525b2453bbedd89f810" start="5"><li data-lake-id="77c32375e387178f828254dff621fe61"><code>space-around</code>:每个项目两侧的间隔相等。所以,<strong>项目之间的间隔比项目与边框的间隔大一倍</strong>。</li></ol><ul data-lake-id="c8a93ae7e8d8fc695bb73651315639ec" data-lake-indent="1"><li data-lake-id="81ed212c0c35dc752d2ff8841f291475" style="padding-left: 6px;"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_ef5750390d4f4e1f9290fd3515fb7b68.png%22%2C%22originWidth%22%3A1274%2C%22originHeight%22%3A244%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A1274%2C%22height%22%3A244%7D"></span></li></ul><h3 data-lake-id="d464b9ee82fe3303faf44c1bbc0c5987" id="LTtrg" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="dec812ad43a34cb1a893e705d11edc24" id="Pao5b" style="padding-left: 9px;">align-items属性</h3><div data-lake-id="d24b4a69daec243a07a13b92bcf69210" style="padding-left: 9px;"><br /></div><div data-lake-id="3014116474a7b9db9fccc17eb1104ee5"><code>align-items</code>属性定义项目在<strong>交叉轴上如何对齐</strong>。</div><ol data-lake-id="ac772c3857c2f53491b8f2cd982033a7"><li data-lake-id="0ed20064ab3edbe4b6298710b36cfc13" style="padding-left: 6px;"><code>stretch</code>(<strong>默认值</strong>):如果<em>项目</em>未设置高度或设为auto,将占满整个容器的高度。</li><li data-lake-id="b7aab505d70bc829ecde7e2dbe4f77c0" style="padding-left: 6px;"><code>flex-start</code>:交叉轴的起点对齐。</li><li data-lake-id="388738a470234f0388d07f6677d3ae6d" style="padding-left: 6px;"><code>flex-end</code>:交叉轴的终点对齐。</li><li data-lake-id="30bd9f2ffa268dbf2e45c2b5cf65ecac" style="padding-left: 6px;"><code>center</code>:交叉轴的中点对齐。</li><li data-lake-id="0193ce163aeff28acc9d94cb0b7a08a2" style="padding-left: 6px;"><code>baseline</code>: 项目的第一行文字的基线对齐。</li></ol><h3 data-lake-id="6de3eaaf28d82f9107194cbedb636aef" id="lY1S4" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="c3f02137f77245cfc8ccbe6515f91815" id="d7iZf" style="padding-left: 9px;">align-content属性</h3><div data-lake-id="7af771f91c89d2e5f99d441c6121ca5c" style="padding-left: 9px;"><br /></div><div data-lake-id="c560ed7b32f76e7fdb4b9f9868c12312"><code>align-content</code>属性定义了<strong>多根轴线的对齐方式</strong>。如果项目只有一根轴线,该属性不起作用。</div><div data-card-type="block" data-ready-card="hr"></div><h2 data-lake-id="b0d74f066bd39a6f8ec1b2138da416a7" id="v6Cp8"><br /></h2><h2 data-lake-id="190f09ebcaf5cf4cccf6b3c5c7ed6962" id="KNGBr">项目的属性(6个)</h2><div data-lake-id="e556780ed8423df9c60c15d523424caa"><br /></div><ol data-lake-id="15cbc0b0ec7a05d4225df9712dd7c9bb"><li data-lake-id="e02c9308362afb469c3e90cd1f1eb6d8" style="padding-left: 6px;"><code>order</code></li><li data-lake-id="c12323e766369ab198897822a0c4bb28" style="padding-left: 6px;"><code>flex-grow</code></li><li data-lake-id="9a34738ad4e432e9e864d2c29f2be4fc" style="padding-left: 6px;"><code>flex-shrink</code></li><li data-lake-id="7b0ef678bb6a6e80bc453bebe33927e6" style="padding-left: 6px;"><code>flex-basis</code></li><li data-lake-id="d60f4103f366a1f7f5439212258f1079" style="padding-left: 6px;"><code>flex</code></li><li data-lake-id="28654138aa1af5128e73f2e18ab106ab" style="padding-left: 6px;"><code>align-self</code></li></ol><h3 data-lake-id="104c81f8b9ce9aee22c2efdf44f30d90" id="jSK7C" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="57e914b4ba4bfc249c47616b98e954d6" id="HFsPo" style="padding-left: 9px;">order</h3><div data-lake-id="4c32103b247879c754fee6dc098598c4" style="padding-left: 9px;"><br /></div><div data-lake-id="fc6fc13cd57ccf03f67be4183546f609"><code>order</code>属性定义项目的排列顺序。<strong>数值越小,排列越靠前,默认为0</strong>。</div><div data-lake-id="34707503abe4d5cce30e3dafb32f9aa8"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_1c212332143a47f298093220b7c135c8.png%22%2C%22originWidth%22%3A751%2C%22originHeight%22%3A480%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A751%2C%22height%22%3A480%7D"></span></div><h3 data-lake-id="e9e8c850dc157596f9ff2496d3253d30" id="8dsWe" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="275162d55e05165272ee4ba6eccee4ea" id="xBJ3d" style="padding-left: 9px;">flex-grow</h3><div data-lake-id="5479a48839216c9805cadabb3ce2626e" style="padding-left: 9px;"><br /></div><div data-lake-id="0676c0e700fbe197089717c09d474f3e"><code>flex-grow</code>属性定义项目的<strong>放大比例</strong>,<strong>默认为0,即如果存在剩余空间,也不放大</strong>。</div><div data-lake-id="cb8c60965105521952a29047d5876bd6">如果<em>所有</em>项目的<code>flex-grow</code>属性都为1,则它们将<strong>等分剩余空间</strong>(如果有的话)。如果一个项目的<code>flex-grow</code>属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。</div><div data-lake-id="135b4e52c83464d47f0c81f44eb43f3b"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_dc55f35bb36c49c18fdaf17992892896.png%22%2C%22originWidth%22%3A802%2C%22originHeight%22%3A211%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A802%2C%22height%22%3A211%7D"></span></div><h3 data-lake-id="25e496a30dfb4c965b41a419cec99a25" id="UVpRf" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="088eb88679334dab372464d87e69f2fa" id="KksBD" style="padding-left: 9px;">flex-shrink</h3><div data-lake-id="d670dcf5e4b10cec8d7b6aed084f1413" style="padding-left: 9px;"><br /></div><div data-lake-id="0e7ffc0ca410a3c67b0738f16572288e"><code>flex-shrink</code>属性定义了项目的<strong>缩小比例</strong>,<strong>默认为1,即如果空间不足,该项目将缩小</strong>。</div><div data-lake-id="db1a1467794cf81b2f1c94465f4928f9">如果所有项目的<code>flex-shrink</code>属性都为1,当空间不足时,都将<strong>等比例缩小</strong>。如果一个项目的<code>flex-shrink</code>属性为0,其他项目都为1,则空间不足时,前者不缩小。</div><div data-lake-id="5f77c1d8aa5305b7e8b42727ceaf233a"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_c9f118705cea48628553667be8ac832c.png%22%2C%22originWidth%22%3A700%2C%22originHeight%22%3A145%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A700%2C%22height%22%3A145%7D"></span></div><h3 data-lake-id="5c8e3b09d11a915951f263bf6fccea67" id="TW2Jf" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="925959a22e6340320d5b1b87194add90" id="UuVZK" style="padding-left: 9px;">flex-basis属性</h3><div data-lake-id="c1e8f37b97e7adba44ff1b4c852bdecd" style="padding-left: 9px;"><br /></div><div data-lake-id="bd57219eb79fa902e2e105df27ab2aa1"><code>flex-basis</code>属性定义了在<strong>分配多余空间之前</strong>,项目占据的<span>{主轴空间|main size}</span>。浏览器根据这个属性,计算主轴是否有多余空间。<strong>它的默认值为auto,即项目的本来大小</strong>。</div><div data-lake-id="7a05fb49cdb9515831a47cd369384474">它可以设为跟<code>width</code>或<code>height</code>属性一样的值(比如350px),则项目<strong>将占据固定空间</strong>。</div><h3 data-lake-id="e90db9d6975acd2ca27c149361dce257" id="V80ui" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="072b0081a1f83de8762773f334ff6584" id="ty4dU" style="padding-left: 9px;">flex</h3><div data-lake-id="a223c8b9fc3633c9449c3b77f9f9ab9e" style="padding-left: 9px;"><br /></div><div data-lake-id="05a594f40eb0ca8a9db645acdb25a965"><code>flex</code>属性是<code>flex-grow</code>, <code>flex-shrink</code> 和 <code>flex-basis</code>的简写,<strong>默认值为0 1 auto</strong>。<strong>后两个属性可选</strong>。</div><div data-lake-id="71b9abe2f4fb567631a2d9e833b73467">由于,后两个属性可选,所以,存在一些比较简洁的写法</div><ol data-lake-id="f5b1a43b5977b00a929472491c194cda"><li data-lake-id="da5b1af055ec36b2f0a9a3f987b5fe04" style="padding-left: 6px;"><strong><code>flex: 1</code> = <code>flex: 1 1 0%</code></strong></li><li data-lake-id="bcc24025c22333116eb96e5c3896a052" style="padding-left: 6px;"><code>flex: 2</code> = <code>flex: 2 1 0%</code></li><li data-lake-id="7085063678d44e7dd739d49010939b86" style="padding-left: 6px;"><code>flex: auto</code> = <code>flex: 1 1 auto</code></li><li data-lake-id="cc922671f4b8b7c085db2443eb0ffcb8" style="padding-left: 6px;"><code>flex: none</code> = <code>flex: 0 0 auto</code>,常用于固定尺寸不伸缩</li></ol><div data-lake-id="3aa4e58682b7c460927f4bbd56959e74"><code>flex:1</code> 和 <code>flex:auto</code> 的区别,可以归结于<code>flex-basis:0</code>和<code>flex-basis:auto</code>的区别</div><div data-lake-id="fac0f8c0afbe228fec52ee4b48e6eeaf">当设置为0时(绝对弹性元素),此时相当于告诉<code>flex-grow</code>和<code>flex-shrink</code>在伸缩的时候不需要考虑我的尺寸</div><div data-lake-id="f1422679f14670f53618a74720a8db5d">当设置为<code>auto</code>时(相对弹性元素),此时则需要在伸缩时将元素尺寸纳入考虑</div><h3 data-lake-id="7d54360171d41925edebfdc9fde28578" id="2K3Lp" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="1f43b9a60652e69c9cca904d0967062a" id="VINkv" style="padding-left: 9px;">align-self属性</h3><div data-lake-id="d9d1b028ad2dd9f8a25db3607fabc968" style="padding-left: 9px;"><br /></div><div data-lake-id="1af9c19ccd661860aac7fe72a9a2fc2a"><code>align-self</code>属性允许<em>单个项目</em>有与其他项目不一样的对齐方式,<strong>可覆盖<code>align-items</code>属性。默认值为auto</strong>,表示继承父元素的<code>align-items</code>属性,如果没有父元素,则等同于<code>stretch</code>。</div><div data-card-type="block" data-ready-card="hr"></div><h1 data-lake-id="ce34d6acc1c9c594f02de944265e5868" id="6ltzj" style="text-align: center;"><br /></h1><h1 data-lake-id="571db46c14f41710e49ebf953bc6bffc" id="Nw5UR" style="text-align: center;">Chrome支持小于12px 的文字</h1><div data-lake-id="df08c4ce1f0460f0b77ebdaa2c3cea27" style="text-align: center;"><br /></div><div data-lake-id="a9a3b344fde3bec80ed3260f550588c2"><code>Chrome</code> <strong>中文版浏览器会默认设定页面的最小字号是12px</strong>,英文版没有限制</div><div data-lake-id="a9afa5d9426316b9da47b6f9a9ee85fa">原由 <code>Chrome</code> 团队认为汉字小于12px就会增加识别难度</div><ul data-lake-id="f706e02f92bb10aabaf32456e4de9356"><li data-lake-id="9eb0447ac5332e8d2be683934c658629">中文版浏览器 <strong>与网页语言无关</strong>,取决于用户在Chrome的设置里(<code>chrome://settings/languages</code>)把哪种语言设置为默认显示语言<br /></li><li data-lake-id="781b0aa41dba9ac19ed1f02678e06d11">系统级最小字号 浏览器默认设定页面的最小字号,用户可以前往 <code>chrome://settings/fonts</code> 根据需求更改<br /></li></ul><h2 data-lake-id="e0e0210dcfd69af35f27c8017ac90c6d" id="F92hY"><br /></h2><h2 data-lake-id="70d8b0f7fc0e9d677547c2c5a31084c4" id="GTWfv">解决方案(3种)</h2><div data-lake-id="54786dc21d61c4263c3cc47a2cc79f70"><br /></div><ol data-lake-id="eec2e6933c7359869a281b4159c22e93"><li data-lake-id="b704b7312523d5a730de4e31e20111db" style="padding-left: 6px;"><code>zoom</code></li><li data-lake-id="de410625a1481447ab976e05cc1fb767" style="padding-left: 6px;"><code>transform:scale()</code></li><li data-lake-id="9ec2a1f04cb9f0ae30379e9becd4db1d" style="padding-left: 6px;"><code>-webkit-text-size-adjust:none</code></li></ol><h3 data-lake-id="5d37227b7fdd2b06c68aa1488a8949fa" id="QTPTF" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="44adadf619a3e876616748ad7e39958d" id="xCrzk" style="padding-left: 9px;">zoom</h3><div data-lake-id="408f79f1a37f68497153ad5b8ca5f8b0" style="padding-left: 9px;"><br /></div><div data-lake-id="204b81f8e3a076130385f4e1becef7f9"><code>zoom</code> 可以改变页面上元素的尺寸,属于真实尺寸。</div><div data-lake-id="6ef268229fdad0106f99a4279c52a21e">其支持的值类型有:</div><ul data-lake-id="4d919b119262568af80114c1dc314656"><li data-lake-id="2c2ef8e97869dffa1bf5c1287c7d1c62"><code>zoom:50%</code>,表示缩小到原来的一半</li><li data-lake-id="e5d653c8d05788bcdc0ae71685ffe97a"><code>zoom:0.5</code>,表示缩小到原来的一半</li></ul><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.span10%7B%5Cn%20%20%20%20%20%20%20%20font-size%3A%2012px%3B%5Cn%20%20%20%20%20%20%20%20display%3A%20inline-block%3B%5Cn%20%20%20%20%20%20%20%20zoom%3A%200.8%3B%5Cn%20%20%20%20%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22KtQ7n%22%7D"></div><h3 data-lake-id="126fbaf12aa31e207cfbfbe586ef491d" id="HJ1mi" style="padding-left: 9px;"><code><br /></code></h3><h3 data-lake-id="a9e3641a7b019636e5c46f15003a602d" id="nemeS" style="padding-left: 9px;"><code>transform:scale()</code></h3><div data-lake-id="91bac2949b5ba4db3c35878ee68eb736" style="padding-left: 9px;"><br /></div><div data-lake-id="f9a0dba8a47e9a3168fcacd5ba555dae">用<code>transform:scale()</code>这个属性进行放缩</div><div data-lake-id="6bee96a208a3b43b706c41c0cce8ca32">使用<code>scale</code>属性<strong>只对可以定义宽高的元素生效</strong>,所以,需要将指定元素转为<em>行内块元素</em></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22.span10%7B%5Cn%20%20%20%20%20%20%20%20font-size%3A%2012px%3B%5Cn%20%20%20%20%20%20%20%20display%3A%20inline-block%3B%5Cn%20%20%20%20%20%20%20%20transform%3Ascale(0.8)%3B%5Cn%20%20%20%20%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22cbQeY%22%7D"></div><h3 data-lake-id="e8362ccfd2be43fd898dc1ba76a32967" id="td1YC" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="a0442228e16bd70b4147e24ff83ceb1b" id="hT5Mi" style="padding-left: 9px;">text-size-adjust</h3><div data-lake-id="eb7b5f798d601c69698d4c01a6dc26c7" style="padding-left: 9px;"><br /></div><div data-lake-id="ab6987d7cfaf290ab61e1e2cbe02ee14">该属性用来设定文字大小是否根据设备(浏览器)来<strong>自动调整显示大小</strong></div><div data-lake-id="d99010c78e71b04c4029c3e93b40434e">属性值:</div><ul data-lake-id="d64d78807c8a9908d735c34fe0461b14"><li data-lake-id="77c64b91653cadaa842d2714b944cb7a"><code>auto</code>:<strong>默认</strong>,字体大小会根据设备/浏览器来自动调整;</li><li data-lake-id="06b91a200d49df895f6d9dcbae2783e8"><code>percentage</code>:字体显示的大小</li><li data-lake-id="22aed9002980fdd8a9884f832cff6367"><code>none</code>:字体大小不会自动调整</li></ul><div data-lake-id="2139b74a738fe2a100e55eee7613e906"><strong>存在兼容性问题,<code>chrome</code>受版本限制,<code>safari</code>可以</strong>。</div><div data-card-type="block" data-ready-card="hr"></div><h1 data-lake-id="97919b3bcd8eafc67d34a5d0348fc3f5" id="wZl1h" style="text-align: center;"><br /></h1><h1 data-lake-id="c32635c87e8b2c1f85bc61f8aab0e40e" id="QgArd" style="text-align: center;">CSS 优化处理 (6个)</h1><div data-lake-id="1a78b5f3b545ee23f98e991730ddd32e" style="text-align: center;"><br /></div><ol data-lake-id="60a0d774d6f03862c44ab4fa3f3c36a3"><li data-lake-id="3fe38201c9d7a306d0b4fc3c951946b2"><strong>内联首屏关键</strong>CSS</li></ol><ul data-lake-id="5e992759a3cd20e0c11f8824f3ca4ffa" data-lake-indent="1"><li data-lake-id="4ff7db6c14e3c2bc46b0267c3f1d3064" style="padding-left: 6px;">但是由于TCP的初始拥塞窗口的原因,导致这种方法只能针对CSS文件小的情况</li></ul><ol data-lake-id="b5d3643c2087f00cf1143dae95aa1859" start="2"><li data-lake-id="9a8a3744164aef4577e2bd60e1ff0836"><strong>异步加载</strong>CSS</li></ol><ul data-lake-id="4761283858509096211a49a7b7e36567" data-lake-indent="1"><li data-lake-id="2378246848f0220207ae164f48a4b127" style="padding-left: 6px;">使用<code>rel="preload"</code>对CSS类资源进行异步加载</li></ul><ol data-lake-id="ba4001eb08f34e91c0849e2fc6e3c796" start="3"><li data-lake-id="dac4c534f780840d33729740b2dd3281" style="padding-left: 6px;">文件压缩</li><li data-lake-id="dd149fc1cfefba1c8e4ef41dc5c232c4">去除无用CSS</li></ol><ol data-lake-id="9cf2eb0351152edc9cc1ae19372e1b1b" data-lake-indent="1"><li data-lake-id="f12af138efd9b58b195e51ea9d0f5288" style="padding-left: 6px;">一种是不同元素或者其他情况下的<em>重复代码</em></li><li data-lake-id="43bd85c2a0a84775509386e44e7635f3" style="padding-left: 6px;">一种是整个页面内<em>没有生效</em>的CSS代码</li></ol><ol data-lake-id="5e26daecc297e21fdab6fdd5b5d0653e" start="5"><li data-lake-id="ddf1c06e036dc1ad26ed278a4a7532cf"><strong>合理使用选择器</strong></li></ol><ul data-lake-id="17a0332709d57cbab3f885e3f3281dd4" data-lake-indent="1"><li data-lake-id="a50eee8151d9acd30b02367803bec670" style="padding-left: 6px;">不要嵌套使用过多复杂选择器,最好不要三层以上</li><li data-lake-id="0fbbe2daee46ba1c4d0de796ebeb130d" style="padding-left: 6px;">使用id选择器就没必要再进行嵌套</li><li data-lake-id="2c36d5d89dfcc381954b5e36978f9f81" style="padding-left: 6px;"><em>通配符</em>和<em>属性选择器</em>效率最低,避免使用</li></ul><ol data-lake-id="744b331c3fb497624d40aa2abb55f5cc" start="6"><li data-lake-id="40fe5db2cdb8434f2900e3cb71fc06d9">不要使用<code>@import</code></li></ol><ul data-lake-id="a4b6b858ec8febfddd650fd5a69836e3" data-lake-indent="1"><li data-lake-id="397836c7ff9394211871be3427781b28">css样式文件有<strong>两种引入方式</strong>,</li></ul><ol data-lake-id="5969ede11d3aea60e9b4cd85810b7e55" data-lake-indent="2"><li data-lake-id="e4d6f53c9ebfb580295e22600de0906e" style="padding-left: 6px;">一种是<code>link</code>元素,</li><li data-lake-id="cef9c06ded1514b51f3e1312992a0798" style="padding-left: 6px;">另一种是<code>@import</code></li></ol><ul data-lake-id="e254272b3b305b82a6fb64a2dce23754" data-lake-indent="1"><li data-lake-id="7539877670a4ebcb5cba9deb99f9c7fe" style="padding-left: 6px;"><code>@import</code>会<strong>影响浏览器的并行下载</strong>,使得页面在加载时增加额外的延迟,增添了额外的往返耗时</li></ul><div data-card-type="block" data-ready-card="hr"></div><h1 data-lake-id="ed5265f03fa7f7aa5c6180c5e42528db" id="O6HEM" style="text-align: center;"><br /></h1><h1 data-lake-id="2a5de6b46264b9f6d224ed7da5b961c3" id="QBCOh" style="text-align: center;">回流、重绘</h1><div data-lake-id="ae69741206249039975d4745e08aeb08" style="text-align: center;"><br /></div><div data-lake-id="cdaad2a2d5875854951e9e54494d2772">页面渲染的流程, 简单来说,初次渲染时会经过以下6步:</div><blockquote style="background-color: #FFF9F9;"><ol data-lake-id="8321dd3c0e090fe57ee13b1820131f02"><li data-lake-id="7cd51a30cc20f283d8c57df446b3f122" style="padding-left: 6px;">构建DOM树;</li><li data-lake-id="98d84f52d4537c21ee1cbd3f6fbf7707" style="padding-left: 6px;">样式计算;</li><li data-lake-id="94cff292cd822cf41d4d511e87556b60" style="padding-left: 6px;"><strong>布局定位</strong>;</li><li data-lake-id="8851e041fa70595e9bdb68419ea8dd8e" style="padding-left: 6px;">图层分层;</li><li data-lake-id="edcb02f8393bad842407fbaca41064b9" style="padding-left: 6px;"><strong>图层绘制</strong>;</li><li data-lake-id="2aac961835bc64e7f0716ba777dc8b6c" style="padding-left: 6px;"><strong>合成显示</strong>;</li></ol></blockquote><div data-lake-id="cef8b4ff686ea914b93abff6c804e661">在CSS属性改变时,重渲染会分为<strong>回流</strong>、<strong>重绘</strong>和<strong>直接合成</strong>三种情况,分别对应从<strong>布局定位</strong>/<strong>图层绘制</strong>/<strong>合成显示</strong>开始,再走一遍上面的流程。</div><div data-lake-id="27b29e1fc9d2f725ce9624b3e8399210">元素的CSS具体发生什么改变,则决定属于上面哪种情况:</div><ul data-lake-id="b32ad618aa76999d76ecb714d2370209"><li data-lake-id="680ce9d4ae978dbffc274c0b6656fa1e">回流(又叫重排):元素<strong>位置、大小</strong>发生变化导致其他节点联动,需要重新计算布局;</li><li data-lake-id="7c75717d2520398b9971d583a8dfbdb0">重绘:修改了一些不影响布局的<strong>属性</strong>,比如颜色;</li><li data-lake-id="e9bdbeb6aa631405591ed79184854ebf">直接合成:<strong>合成层</strong>的<code>transform、opacity</code>修改,只需要将多个图层<strong>再次合并</strong>,而后<strong>生成位图</strong>,最终展示到屏幕上;</li></ul><h2 data-lake-id="8e066afdbce4be85a2c6b1486f197162" id="eErKZ"><br /></h2><h2 data-lake-id="be400d5da0ba62795abe02c88fb969be" id="lDo7v">触发时机</h2><div data-lake-id="87fdf9da3c693b325ae2aa9f742309ce"><br /></div><h3 data-lake-id="f17c8d9d4e60a6701c8d0fb5f29aa0c5" id="TlwLi" style="padding-left: 9px;">回流触发时机</h3><div data-lake-id="27ef34dd3a9a5cc20cf0c8a21d784dfb" style="padding-left: 9px;"><br /></div><div data-lake-id="7d5ace963ec7ca5901abde83968fbf56">回流这一阶段主要是<em>计算节点的位置和几何信息</em>,那么当页面布局和几何信息发生变化的时候,就需要回流。</div><ul data-lake-id="6c3a33f39e07233f9e3ed1f02e26c244"><li data-lake-id="11f803fbedd895816a291fe91785a389">添加或删除<strong>可见的DOM元素</strong></li><li data-lake-id="561f7698c6ee6764ba72bdfe9a949931">元素的<strong>位置</strong>发生变化</li><li data-lake-id="92d75f1a1ca851ad0b69add0d7fbe4cc">元素的<strong>尺寸</strong>发生变化(包括外边距、内边框、边框大小、高度和宽度等)</li><li data-lake-id="29dfc86d578a65bd7d90198a5abfe231">内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代</li><li data-lake-id="ab462da33910ceff436f414ad859d6f2">页面一开始渲染的时候(这避免不了)</li><li data-lake-id="9078cfd736f2619d0db3e1b8dc0ef1e9">浏览器的<strong>窗口尺寸变化</strong>(因为回流是根据视口的大小来计算元素的位置和大小的)</li><li data-lake-id="7ecfcbc34be1d4e77e83d6130b3097f6">获取一些特定属性的值</li></ul><ul data-lake-id="b0b2a141b08f639ac8ff098ebc56daf2" data-lake-indent="1"><li data-lake-id="5602e855d9091cea5de2cf14c62ac144"><code>offsetTop、offsetLeft、 offsetWidth、offsetHeight</code></li><li data-lake-id="ff952c4befd5e7ae498bf96f1523def7"><code>scrollTop、scrollLeft、scrollWidth、scrollHeight</code></li><li data-lake-id="44e26bd732b7f6478d5c9e1a45e9f14e"><code>clientTop、clientLeft、clientWidth、clientHeight</code></li><li data-lake-id="8cd8156f3f80d8c95f0112c94b9dcefa">这些属性有一个共性,就是需要通过<strong>即时计算</strong>得到。因此浏览器为了获取这些值,也会进行回流。</li></ul><h3 data-lake-id="a557d15b3a6786d6a681d7a5ef9f4ea2" id="lJ7Ak" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="02a60dc248b0eaa7cec2eace71c88d04" id="d5gjZ" style="padding-left: 9px;">重绘触发时机</h3><div data-lake-id="d3d52c8a22dae1de8b27078055af039a" style="padding-left: 9px;"><br /></div><blockquote style="background-color: #FFF9F9;"><div data-lake-id="3e8098801f794cabaf6bf831513bb946">触发回流一定会触发重绘</div></blockquote><div data-lake-id="9a77d3c86468642e7046e36969950ee0">除此之外还有一些其他引起重绘行为:</div><ul data-lake-id="3178c264e9a74e37380389db25845def"><li data-lake-id="a5472137feef7b3c76f831f0e44c7081"><strong>颜色</strong>的修改</li><li data-lake-id="aa3218de19c8612cb760534db5ea1b63"><strong>文本方向</strong>的修改</li><li data-lake-id="51a5cb95f30857ae849f4916123d2445"><strong>阴影</strong>的修改</li></ul><h2 data-lake-id="459c7442761cff8894d276a65575be61" id="sjROt"><br /></h2><h2 data-lake-id="506c7495415cdabc235c5321cddd12b0" id="UwvOG">浏览器优化机制</h2><div data-lake-id="6f2df852e065cd0b39ef1ac74aff8cf3"><br /></div><div data-lake-id="86649c32b68ca5355962a5590bc53961">由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会<strong>通过队列存储重排操作并批量执行来优化重排过程</strong>。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。</div><div data-lake-id="33f2ad4abbf2d8e1c11b1ff45dc7d0ce">当你获取布局信息的操作的时候,会<strong>强制队列刷新</strong>,例如<code>offsetTop</code>等方法都会返回最新的数据。</div><div data-lake-id="bf079e65c74be306c024d2bc2422c361">因此浏览器不得不清空队列,触发回流重绘来返回正确的值</div><h2 data-lake-id="88ba0c14a6d62b5e577cd04712559356" id="5ch5l"><br /></h2><h2 data-lake-id="cffd64f947224146c2d9f575a9346234" id="LoOrG">减少回流</h2><div data-lake-id="7194c27bffae5d8551bce6024b7dc9de"><br /></div><ol data-lake-id="9308541f659ae125137a284233d31ffd"><li data-lake-id="6a7198af4839f4da79b6db9aae7cc5c4" style="padding-left: 6px;">对于那些复杂的动画,对其设置 <code>position: fixed/absolute</code>,尽可能地使元素脱离文档流,从而减少对其他元素的影响</li><li data-lake-id="cd50de37177804ee83aa57bb2f43f333" style="padding-left: 6px;">使用css3<strong>硬件加速</strong>,可以让<code>transform</code>、<code>opacity</code>、<code>filters</code>这些动画不会引起回流重绘</li><li data-lake-id="2e6840e6b7d58467bdb55ed74599bc8f" style="padding-left: 6px;">在使用 <code>JavaScript</code> <strong>动态插入多个节点</strong>时, 可以使用<code>DocumentFragment</code>.创建后一次插入.</li><li data-lake-id="58bf6204280431caa6dd85bc7bb67a7e" style="padding-left: 6px;">通过设置元素属性<code>display: none</code>,将其从页面上去掉,然后再进行后续操作,这些后续操作也不会触发回流与重绘,这个过程称为离线操作</li></ol><div data-card-type="block" data-ready-card="hr"></div><h1 data-lake-id="afd03dba3b26ba3dae470c1e67ef85cf" id="pqBdn" style="text-align: center;"><br /></h1><h1 data-lake-id="c8d946b43b0020ffc1114b9f243176b3" id="W51E1" style="text-align: center;">硬件加速</h1><div data-lake-id="be19480089466cf787b3f26588fce0ff" style="text-align: center;"><br /></div><div data-lake-id="1e1521d397ed890470cdd8d60fade4c5">浏览器中的层分为两种:<strong>渲染层</strong>和<strong>合成层</strong>。</div><h2 data-lake-id="5191782f40cddbfe3e991e703083a4b2" id="ZhBwK"><br /></h2><h2 data-lake-id="1dde0138fac0ec46e289762c45c0d2ce" id="ub2J2">渲染层</h2><div data-lake-id="d6f305c57d389657a4822fca57535f66"><br /></div><div data-lake-id="4acbc79ec5f4c922a55fd05d3af7bf7b">渲染层的概念跟<em>层叠上下文</em>密切相关。简单来说,拥有<code>z-index</code>属性的<em>定位元素</em>会生成一个层叠上下文,一个生成层叠上下文的元素就生成了一个渲染层。</div><h3 data-lake-id="21676edfc611ed65324f25ec7f98fe42" id="IOkOe" style="padding-left: 9px;"><br /></h3><h3 data-lake-id="7e3bea2afda85bf5d9c67c1c28c5660e" id="e9R02" style="padding-left: 9px;">层叠上下文的创建(3类)</h3><div data-lake-id="5e3bb518f9aee30b561d0c2a06d19891" style="padding-left: 9px;"><br /></div><div data-lake-id="bf95a6b1648937d809d015481529e788">由一些CSS属性创建</div><ol data-lake-id="cce217d7ec4f43b064d0a579c4e71248"><li data-lake-id="59facdc3ddbf0a1a5a532ae4678cc8a4">天生派</li></ol><ul data-lake-id="1c802e1721c185faa1c8b387e1bbd070" data-lake-indent="1"><li data-lake-id="ce6e02404872cdb321f7395ae3f23f7f" style="padding-left: 6px;">页面根元素天生具有层叠上下文</li><li data-lake-id="73d536e3804b438002dd7024adc2887b" style="padding-left: 6px;">根层叠上下文</li></ul><ol data-lake-id="5b2dba879dad8517c3a3fcd18793e130" start="2"><li data-lake-id="3d3085563524f8ccc897de09840f0113">正统派</li></ol><ul data-lake-id="60cf018aed714778a7f882527a915f6c" data-lake-indent="1"><li data-lake-id="48676e9049af92c090af421d9b52f579" style="padding-left: 6px;"><code>z-index</code>值为数值的<em>定位元素</em>的传统层叠上下文</li></ul><ol data-lake-id="074cd3baa9992fb40b8647347521828c" start="3"><li data-lake-id="43909641326ff44b2538c9f4316dbf9b">扩招派 (CSS3属性)</li></ol><ol data-lake-id="e37da99969cf37046afa861f2a777360" data-lake-indent="1"><li data-lake-id="8d9242558fd4e87593a99e358f979aea" style="padding-left: 6px;">元素为<code>flex</code>布局元素(父元素<code>display:flex|inline-flex</code>),同时<code>z-index</code>值<strong>不是auto</strong> - <strong>flex布局</strong></li><li data-lake-id="0856cbe5123d7c11fd5a9d6e9898f95a" style="padding-left: 6px;">元素的<code>opactity</code>值不是1 - <span>{透明度|opactity}</span></li><li data-lake-id="d04602775778d9a9460f39642b993d36" style="padding-left: 6px;">元素的<code>transform</code>值不是<code>none</code> - <span>{转换|transform}</span></li><li data-lake-id="3cfcb416faf3b13fa027c07f97954ad2" style="padding-left: 6px;">元素<code>mix-blend-mode</code>值不是<code>normal</code> - <span>{混合模式|mix-blend-mode}</span></li><li data-lake-id="2ed4ebcf3b7bd65f69610442ea8a19d3" style="padding-left: 6px;">元素的<code>filter</code>值不是<code>none</code> - <span>{滤镜|filter}</span></li><li data-lake-id="87393af8c5530173fe80ca385de33051" style="padding-left: 6px;">元素的<code>isolation</code>值是<code>isolate</code> - <span>{隔离|isolation}</span></li><li data-lake-id="abcd06fd82869175ae86382a29698590" style="padding-left: 6px;">元素的<code>will-change</code>属性值为上面②~⑥的任意一个(如<code>will-change:opacity</code>)</li><li data-lake-id="1266d119ac71809fe48d1e5edf64fee5" style="padding-left: 6px;">元素的<code>-webkit-overflow-scrolling</code>设为<code>touch</code></li></ol><h2 data-lake-id="e84faa185db248480884c75ed8c048dd" id="sGqc5"><br /></h2><h2 data-lake-id="71cc5b0e113cdef6bb22b4ee8d7ee80b" id="mbLBW">合成层</h2><div data-lake-id="3c56f0dd4cbe951fc6bd93d153cfce6a"><br /></div><div data-lake-id="39951269f4df07bb595fc829f24b289a"><strong>只有一些特殊的渲染层才会被提升为合成层</strong>,通常来说有这些情况:</div><ol data-lake-id="39a827cc5814f33896a70b615d19e4fe"><li data-lake-id="2c116c56bdc78a50b9a0ad8c6882fde3" style="padding-left: 6px;"><code>transform:3D</code>变换:<code>translate3d</code>,<code>translateZ</code>;</li><li data-lake-id="b99a6b6e685436bd53827383a4bec1c5" style="padding-left: 6px;"><code>will-change:opacity | transform | filter</code></li><li data-lake-id="3504a08d01a4b800f8457eb0598f949f" style="padding-left: 6px;">对 <code>opacity</code> | <code>transform</code> | <code>fliter</code> 应用了过渡和动画(<code>transition/animation</code>)</li><li data-lake-id="31fcbbe9c45de75c25b6a79b20aba3f9" style="padding-left: 6px;"><code>video、canvas、iframe</code></li></ol><h2 data-lake-id="7e0b746d773a60cec3cdc3dd23982e2f" id="a0Ah3"><br /></h2><h2 data-lake-id="8b3ab3f85b1f9b5fb99d70837d24c419" id="SkcEo">硬件加速</h2><div data-lake-id="c80b51883e00f7a177b2008c835a2b6f"><br /></div><div data-lake-id="ee68ff5e46ee3f2b5f3ae5af3eebebda">浏览器为什么要分层呢?答案是<strong>硬件加速</strong>。就是给HTML元素加上某些CSS属性,比如3D变换,将其提升成一个合成层,<strong>独立渲染</strong>。</div><div data-lake-id="81afe6657ebe76935addc4150260c7e9">之所以叫硬件加速,就是因为<strong>合成层会交给GPU(显卡)去处理</strong>,在硬件层面上开外挂,比在主线程(CPU)上效率更高。</div><div data-lake-id="dbed5972a0fc3bb9c0fb464fb8b6374f">利用硬件加速,可以把需要重排/重绘的元素单独拎出来,减少绘制的面积。</div><div data-lake-id="178c08398ab428280e58a6e0b9e0b3ea">避免重排/重绘,直接进行合成,合成层的<code>transform</code> 和 <code>opacity</code>的修改都是直接进入合成阶段的;</div><ul data-lake-id="1acf3d3c92f3b27befb61c619d4db506"><li data-lake-id="a15ec0d1f0c46a34e3ffde83871748ad">可以使用<code>transform:translate</code>代替<code>left/top</code>修改元素的位置;</li><li data-lake-id="2928c61896653a162d2a08da80afc947">使用<code>transform:scale</code>代替<em>宽度、高度</em>的修改;</li></ul><div data-card-type="block" data-ready-card="hr"></div><h1 data-lake-id="ef83302079e40c98d2477a0d2664e427" id="el2H0" style="text-align: center;"><br /></h1><h1 data-lake-id="e6dc95097783ecb85e392bef75a471d2" id="TtMOW" style="text-align: center;">Css预编译语言</h1><div data-lake-id="69ef00be0d5e7114401555b78714434a" style="text-align: center;"><br /></div><div data-lake-id="a328df4dd12cf602e02f937e4563cc38">Css预编译语言在前端里面有三大优秀的预编处理器,分别是:</div><ol data-lake-id="467e35dd6fa0f5933c948df9c4faae62"><li data-lake-id="e93880a923cd283683469481c723076d" style="padding-left: 6px;"><code>sass</code></li><li data-lake-id="2319e2955f3bc413219d2e29fb35d60e" style="padding-left: 6px;"><code>less</code></li><li data-lake-id="1e9663f9bdc086f8f30ece65aa399ba7" style="padding-left: 6px;"><code>stylus</code></li></ol><div data-lake-id="e4176b30182c2ac2751174d444f3c7fd">虽然各种预处理器功能强大,但使用最多的,还是以下特性:</div><ol data-lake-id="e64be7862b42dcd0cf3cb2706b6420d5"><li data-lake-id="c509c07e793bf39801c2aac18372d2ad">变量(<code>variables</code>)</li></ol><ul data-lake-id="7015db409fcddd8696a9754057ebd06d" data-lake-indent="1"><li data-lake-id="8d6abfd5880d759013347b8a148e3a7d" style="padding-left: 6px;"><code>less</code>声明的变量必须以<code>@</code>开头,后面紧跟变量名和变量值,而且变量名和变量值需要使用冒号<code>:</code>分隔开</li><li data-lake-id="6e2c5b4547a3e098a951d66707599a5a" style="padding-left: 6px;"><code>sass</code>声明的变量名前面使用<code>$</code>开头</li></ul><ol data-lake-id="a3ef6b2f999a83c5b42ccd07040283b8" start="2"><li data-lake-id="e93be2e0a2b604ed4a76a3ad17414a35" style="padding-left: 6px;">作用域(<code>scope</code>)</li><li data-lake-id="e4bb2d7fa9fb4a53df5978af9fc98028" style="padding-left: 6px;">代码混合( <code>mixins</code>)</li><li data-lake-id="6cf391fccd9512e7eec707c67811c040" style="padding-left: 6px;">嵌套(<code>nested rules</code>)</li><li data-lake-id="4a4a9c96efb73009380f6f08748436b1">代码模块化(<code>Modules</code>)</li></ol><ul data-lake-id="643c5e0213f68c0e4da2ba40aadedce2" data-lake-indent="1"><li data-lake-id="147124ae52964d3d50a9802a3e19eb96" style="padding-left: 6px;">模块化就是将Css代码分成一个个模块</li><li data-lake-id="31803fe45d007829cb1f782cad1114cd" style="padding-left: 6px;"><code>@import</code></li></ul><div data-card-type="block" data-ready-card="hr"></div><h1 data-lake-id="4ef5b7fe29ac39f493622305ca713da4" id="4TJWy" style="text-align: center;"><br /></h1><h1 data-lake-id="c30805836ab1755d970b7ae134116cf6" id="aydwE" style="text-align: center;">后记</h1><div data-lake-id="f3b58ef4e6ed7df99774449672708d0b" style="text-align: center;"><br /></div><div data-lake-id="62f0e3ed60279aab2a4fb1f3abd10ad1"><strong>分享是一种态度</strong>。</div><div data-lake-id="253279ad894fb16873cffc3544320f82"><strong>全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。</strong></div><div data-lake-id="277f4da79fca84f9f4bb1d581ac5ff1e"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_abc8eae954af4887805f748d4f62a071.gif%22%2C%22originWidth%22%3A413%2C%22originHeight%22%3A390%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A413%2C%22height%22%3A390%7D"></span></div><div data-lake-id="b4b6cc41ae31a6ae67f704b8f429fa7d"><br /></div>
打不垮我的,将使我更加坚强 --尼采大家好,我是柒八九。好久没更文了,其实这段时间,一直没闲着。在准备一些比较重要的东西。忙着跑步,忙着学习,忙着xx。 总之就是,一直在忙着,从未停歇。虽然,这段时间,没有文章的发布,其实,在私底下,已经有不下10篇的文章已经起手了。等再润色一下,就会和大家见面。这是,我之前学习总结,后期会逐步给大家免费分享。敬请期待。好了,闲话少叙。今天,我们来谈谈关于-- Webpack的打包优化。你能所学到的知识点Webpack Loader 和 Plugin 的区别Webpack 生命周期Webpack编译阶段提效减少执行编译的模块提升单个模块构建的速度并行构建以提升总体效率Webpack打包阶段提效以提升当前任务工作效率为目标的方案压缩 Chunk 产物代码以提升后续环节工作效率为目标的方案Code SplittingTree ShakingScope Hoisting (作用域提升)sideEffects缓存优化Webpack Loader vs Pluginloader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中plugin 赋予了 webpack 各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决 loader 无法实现的其他事两者在运行时机上的区别loader 运行在打包文件之前plugins 在整个编译周期都起作用对于 loader,实质是一个转换器,将A文件进行编译形成B文件,操作的是文件,比如将A.scss或A.less转变为B.css,单纯的文件转换过程。在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过Webpack提供的 API改变输出结果。Webpack 生命周期Webpack 工作流程中最核心的两个模块CompilerCompilation它们都扩展自 Tapable 类,用于实现工作流程中的生命周期划分,以便在不同的生命周期节点上注册和调用插件,其中所暴露出来的生命周期节点称为Hook(俗称钩子)。Compiler Hooks构建器实例的生命周期可以分为 3 个阶段初始化阶段构建过程阶段产物生成阶段初始化阶段environment在创建完 compiler 实例且执行了配置内定义的插件的 apply 方法后触发afterEnvironment在创建完 compiler 实例且执行了配置内定义的插件的 apply 方法后触发entryOption执行 EntryOptions 插件afterPluginsafterResolvers解析了 resolver 配置后触发构建过程阶段normalModuleFactory在两类模块工厂创建后触发contextModuleFactory在两类模块工厂创建后触发beforeRunrunbeforeCompilecompilethisCompilationmake最耗时会执行模块编译到优化的完整过程产物生成阶段shouldEmit、emit、assetEmitted、afterEmit在构建完成后,处理产物的过程中触发failed、done在达到最终结果状态时触发Compilation Hook构建过程实例的生命周期分为两个阶段:构建阶段优化阶段Webpack编译阶段提效真正影响整个构建效率的是 Compilation 实例的处理过程编译模块优化处理要提升编译阶段的构建效率,大致可以分为三个方向减少执行编译的模块提升单个模块构建的速度并行构建以提升总体效率优化前的准备工作准备基于时间的分析工具 - SMP需要一类插件,来帮助我们统计项目构建过程中在编译阶段的耗时情况speed-measure-webpack-pluginconst SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); const smp = new SpeedMeasurePlugin(); const webpackConfig = smp.wrap({ plugins: [new MyPlugin(), new MyOtherPlugin()], }); 复制代码准备基于产物内容的分析工具 - WBA找出对产物包体积影响最大的包的构成,从而找到那些冗余的、可以被优化的依赖项。不仅能减小最后的包体积大小,也能提升构建模块时的效率webpack-bundle-analyzer const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = { plugins: [ new BundleAnalyzerPlugin() ] } 复制代码编译模块阶段所耗的时间是从单个入口点开始,编译每个模块的时间的总和减少执行编译的模块(4个)IgnorePlugin (国际化包)按需引入类库模块 (工具类库)DllPluginExternalsIgnorePlugin有的依赖包,除了项目所需的模块内容外,还会附带一些多余的模块Webpack 提供的 IgnorePlugin ,即可在构建模块时直接剔除那些需要被排除的模块,从而提升构建模块的速度,并减少产物体积。new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/, }); 复制代码resourceRegExp指定需要剔除的文件(夹)contextRegExp (可选)特定目录任何以 'moment' 结尾的目录中匹配 './locale' 的任何 require 语句都将被忽略除了 moment 包以外,其他一些带有国际化模块的依赖包,都可以应用这一优化方式。按需引入类库模块减少执行模块的方式是按需引入,一般适用于工具类库性质的依赖包的优化优化处理定向引入效果最佳的方式是在导入声明时只导入依赖包内的特定模块使用插件babel-plugin-lodashbabel-plugin-import 适用于antd,antd-mobil,lodash{ "plugins": [["import",{ "libraryName": "lodash", "libraryDirectory": "", "camel2DashComponentName": false, // default: true }]] } 复制代码注意点Tree Shaking,这一特性也能减少产物包的体积,但是 Tree Shaking 需要相应导入的依赖包使用 ES6 模块化,而 lodash 还是基于 CommonJS ,需要替换为 lodash-es 才能生效Tree Shaking 是在优化阶段生效,Tree Shaking 并不能减少模块编译阶段的构建时间。DllPlugin它的核心思想是将项目依赖的框架等模块单独构建打包,与普通构建流程区分开。事先把常用但又构建时间长的代码提前打包好(例如 react、react-dom),取个名字叫 dll。后面再打包的时候就跳过原来的未打包代码,直接用 dll。这样一来,构建时间就会缩短,提高 webpack 打包速度。两个配置文件webpack.dll.config.jsmodule.exports = { entry: { vendor: ['react', 'react-dom'], }, output: { filename: '[name].dll.js', path: path.join(__dirname, 'dll'), publicPath: '/dll', library: '[name]_dll', }, plugins: [ new webpack.DllPlugin({ context: __dirname, name: '[name]_dll', path: path.join(__dirname, 'dll' + '/[name]_manifest.json'), }), ], } 复制代码new webpack.DllPlugin - 生成manifest.json文件,供DllReferencePlugin 指向依赖模块位置 - 将公共模块 react/react-dom 抽离到项目中dll文件下webpack.app.config.jsplugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require('./dll/vendor_manifest.json'), }), ], 复制代码new webpack.DllReferencePlugin引用 manifest.json文件,寻找依赖模块webpack 4 有着比 dll 更好的打包性能,所以在最新版的cra中已经将dll剔除。ExternalsWebpack 配置中的 externals 和 DllPlugin 解决的是同一类问题。将依赖的框架等模块从构建过程中移除。Externals 和 DllPlugin 区别配置方面externals 更简单DllPlugin 需要独立的配置文件DllPlugin 包含了依赖包的独立构建流程,而 externals 配置中不包含依赖框架的生成方式,通常使用已传入 CDN 的依赖包externals 配置的依赖包需要单独指定依赖模块的加载方式:全局对象、CommonJS、AMD 等在引用依赖包的子模块时,DllPlugin 无须更改,而 externals 则会将子模块打入项目包中使用范例module.exports = { //... externals: [ { // String react: 'react', // Object lodash: { commonjs: 'lodash', amd: 'lodash', root: '_', // indicates global variable }, // [string] subtract: ['./math', 'subtract'], }, // Function function ({ context, request }, callback) { if (/^yourregex$/.test(request)) { return callback(null, 'commonjs ' + request); } callback(); }, // Regex /^(jquery|\$)$/i, ], }; 复制代码提升单个模块构建的速度在保持构建模块数量不变的情况下,提升单个模块构建的速度。常用的方式有include/excludenoParseSource MapTypeScript 编译优化Resolve通过减少构建单个模块时的一些处理逻辑来提升速度include/excludeWebpack -loader配置中的 include/exclude,是常用的优化特定模块构建速度的方式之一include 的用途是只对符合条件的模块使用指定 Loader 进行转换处理exclude 则相反,不对特定条件的模块使用该 Loader例如不使用 babel-loader 处理 node_modules 中的模块 使用范例module.exports = { ...... module: { rules: [ { test: /\.js$/, include: /src/ exclude: /node_modules/, use: ['babel-loader'], }, ], }, } 复制代码注意点通过 include/exclude 排除的模块,并非不进行编译,而是使用 Webpack 默认的 js 模块编译器进行编译在一个 loader 中的 include 与 exclude 配置存在冲突的情况下,优先使用 exclude 的配置,而忽略冲突的 include 部分的配置noParseWebpack 配置中的 module.noParse 则是在 include/exclude 的基础上,进一步省略了使用默认 js 模块编译器进行编译的时间使用范例module.exports = { ...... module: { noParse: /jquery|lodash/, rules: [ { test: /\.js$/, use: ['babel-loader'], }, ], }, } 复制代码Source Map对于生产环境的代码构建而言,会根据项目实际情况判断是否开启 Source Map在开启 Source Map 的情况下,优先选择与源文件分离的类型 --例如 "source-map"TypeScript 编译优化Webpack 中编译 TS 有两种方式使用 ts-loader使用 babel-loader在使用 ts-loader 时,由于 ts-loader 默认在编译前进行类型检查,因此编译时间往往比较慢通过加上配置项 transpileOnly: true,可以在编译时忽略类型检查module.exports = { ...... module: { rules: [ { test: /\.ts$/, use: { loader: 'ts-loader', options: { transpileOnly: true, }, }, }, ], }, } 复制代码babel-loader 则需要单独安装 @babel/preset-typescript 来支持编译 TS,配合 ForkTsCheckerWebpackPlugin 使用类型检查功能module.exports = { ...... module: { rules: [ { test: /\.ts$/, use: ['babel-loader'], }, ], }, plugins: [ new TSCheckerPlugin({ typescript: { diagnosticOptions: { semantic: true, syntactic: true, }, }, }), ], } 复制代码ResolveWebpack 中的 resolve 配置制定的是在构建时指定查找模块文件的规则resolve.modules指定查找模块的目录范围resolve.extensions指定查找模块的文件类型范围resolve.mainField指定查找模块的 package.json 中主文件的属性名resolve.symlinks指定在查找模块时是否处理软连接这些规则在处理每个模块时都会有所应用,因此尽管对小型项目的构建速度来说影响不大,对于大型的模块众多的项目而言,使用默认配置和增加了大量无效范围后,构建时长的变化。并行构建以提升总体效率并行构建的方案早在 Webpack 2 时代已经出现,适用于大项目。 使用方式HappyPackthread-loaderparallel-webpackHappyPack 与 thread-loader两种工具的本质作用相同,都作用于模块编译的 Loader 上,用于在特定 Loader 的编译过程中。开启多进程的方式加速编译module.exports = { module: { rules: [ { test: /\.js$/, include: path.resolve('src'), use: [ 'thread-loader', ’babel-loader‘ ], }, ], }, }; 复制代码parallel-webpack并发构建的第二种场景是针对与多配置构建。Webpack 的配置文件可以是一个包含多个子配置对象的数组,在执行这类多配置构建时,默认串行执行var path = require('path'); module.exports = [ { entry: './pageA.js', output: { path: path.resolve(__dirname, './dist'), filename: 'pageA.bundle.js' } }, { entry: './pageB.js', output: { path: path.resolve(__dirname, './dist'), filename: 'pageB.bundle.js' } }]; 复制代码通过 parallel-webpack,就能实现相关配置的并行处理"build:parallel": "parallel-webpack --config webpack.parallel.config.js" 复制代码Webpack打包阶段提效Webpack 构建流程中的第二个阶段,也就是从代码优化到生成产物阶段的效率提升问题优化阶段可以分为两个不同的方向针对某些任务使用效率更高的工具或配置项从而提升当前任务的工作效率提升特定任务的优化效果以减少传递给下一任务的数据量从而提升后续环节的工作效率以提升当前任务工作效率为目标的方案一般在项目的优化阶段,主要耗时的任务有两个生成ChunkAssets即根据 Chunk 信息生成 Chunk 的产物代码主要在Webpack引擎内部的模块中处理优化手段较少主要集中在利用缓存方面优化 Assets即压缩 Chunk 产物代面向 JS 的压缩工具Webpack 4 中内置了 TerserWebpackPlugin 作为默认的 JS 压缩工具--基于 Terser。之前的版本,需要单独引入,早期主要使用的是 UglifyJSWebpackPlugin-- 基于 UglifyJS 。两者在压缩效率与质量方面差别不大,但 Terser 整体上略胜一筹Terser 和 UglifyJS 插件中的效率优化Terser 原本是 Fork 自 uglify-es 的项目,其绝大部分的 API 和参数都与 uglify-es 和 uglify-js@3 兼容。以 Terser 为例来分析其中的优化方向npm install terser-webpack-plugin --save-dev 复制代码TerserWebpackPlugin 中,对执行效率产生影响的配置主要分为 3 个方面Cache选项默认开启使用缓存能够极大程度上提升再次构建时的工作效率Parallel选项默认开启并发选项在大多数情况下能够提升该插件的工作效率适用大项目terserOptions选项即 Terser 工具中的 minify 选项集合主要看其中的compress和mangle选项compress参数的作用执行特定的压缩策略例如省略变量赋值的语句,从而将变量的值直接替换到引入变量的位置上,减小代码体积在需要对压缩阶段的效率进行优化的情况下,可以优先选择设置该参数mangle参数的作用对源代码中的变量与函数名称进行压缩当compress参数为 false 时,压缩阶段的效率有明显提升,同时对压缩的质量影响较小案例使用module.exports = { optimization: { minimize: true, minimizer: [ new TerserPlugin({ cache: false, terserOptions: { compress: false, mangle: false, }, }), ], }, }; 复制代码压缩代码是在 optimizeChunkAssets 阶段面向 CSS 的压缩工具CSS 同样有3种压缩工具可供选择OptimizeCSSAssetsPluginCRA中使用OptimizeCSSNanoPluginvue-cliCssMinimizerWebpackPlugin2020 年 Webpack 社区新发布的 CSS 压缩插件它们都基于 cssnano 实现,压缩质量方面没有什么差别。在压缩效率方面,最新发布的 MiniCssExtractPlugin,它支持缓存和多进程,默认开启多进程。这是另外两个工具不具备的。MiniCssExtractPlugin对于 CSS 文件的打包,一般我们会使用 style-loader 进行处理,这种处理方式最终的打包结果就是 CSS 代码会内嵌到 JS 代码中MiniCssExtractPlugin是一个可以将 CSS 代码从打包结果中提取出来的插件。// ./webpack.config.js const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { module: { rules: [ { test: /\.css$/, use: [ // 'style-loader', // 将样式通过 style 标签注入 MiniCssExtractPlugin.loader, 'css-loader' ] } ] }, plugins: [ new MiniCssExtractPlugin() ] } 复制代码将这个插件添加到配置对象的 plugins 数组中,使用 MiniCssExtractPlugin中提供的 loader 去替换掉 style-loader,以此来捕获到所有的样式。打包过后,样式就会存放在独立的文件中,直接通过 link 标签引入页面CssMinimizerWebpackPlugin (webpack 5)使用了 MiniCssExtractPlugin 过后,样式就被提取到单独的 CSS 文件中了,样式文件并没有被压缩。Webpack 内置的压缩插件仅仅是针对 JS 文件的压缩,其他资源文件的压缩都需要额外的插件。// ./webpack.config.js const MiniCssExtractPlugin = require('mini-css-extract-plugin') const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); module.exports = { optimization: { minimizer: [ new CssMinimizerPlugin() ] }, module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader' ] } ] }, plugins: [ new MiniCssExtractPlugin() ] } 复制代码文档中的这个插件并不是配置在 plugins 数组中的,而是添加到了 optimization 对象中的 minimizer 属性中。如果我们配置到 plugins 属性中,那么这个插件在任何情况下都会工作,而配置到 minimizer 中,就只会在 minimize 特性开启时才工作 --- optimization.minimize: true原本可以自动压缩的 JS,现在却不能压缩了,因为设置了 minimizer。Webpack 认为我们需要使用自定义压缩器插件,那内部的 JS 压缩器就会被覆盖掉。必须手动再添加回来内置的 JS 压缩插件叫作 terser-webpack-plugin,手动添加这个模块到 minimizer 配置当中。// ./webpack.config.js const MiniCssExtractPlugin = require('mini-css-extract-plugin') const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const TerserWebpackPlugin = require('terser-webpack-plugin') module.exports = { optimization: { minimize: true, minimizer: [ new TerserWebpackPlugin(), new CssMinimizerPlugin() ] }, module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader' ] } ] }, plugins: [ new MiniCssExtractPlugin() ] } 复制代码以提升后续环节工作效率为目标的方案通过对本环节的处理减少后续环节处理内容,以便提升后续环节的工作效率Code SplittingTree ShakingScope Hoisting (作用域提升)sideEffectsCode Splitting(分块打包)Code Splitting--通过把项目中的资源模块按照我们设计的规则打包到不同的 bundle 中降低应用的启动成本提高响应速度Webpack 实现分包的方式主要有两种根据业务不同配置多个打包入口,输出多个打包结果结合 ES Modules 的动态导入(Dynamic Imports)特性,按需加载模块多入口打包多入口打包一般适用于传统的多页应用程序,最常见的划分规则就是:一个页面对应一个打包入口,对于不同页面间公用的部分,再提取到公共的结果中├── dist ├── src │ ├── common │ │ ├── fetch.js │ │ └── global.css │ ├── album.css │ ├── album.html │ ├── album.js │ ├── index.css │ ├── index.html │ └── index.js ├── package.json └── webpack.config.js 复制代码有两个页面,分别是 index 和 albumindex.js 负责实现 index 页面功能逻辑album.js 负责实现 album 页面功能逻辑global.css 是公用的样式文件fetch.js 是一个公用的模块,负责请求 API配置文件// ./webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { entry: { index: './src/index.js', album: './src/album.js' }, output: { filename: '[name].bundle.js' // [name] 是入口名称 }, // ... 其他配置 plugins: [ new HtmlWebpackPlugin({ title: 'Multi Entry', template: './src/index.html', filename: 'index.html' }), new HtmlWebpackPlugin({ title: 'Multi Entry', template: './src/album.html', filename: 'album.html' }) ] } 复制代码一般entry属性中只会配置一个打包入口。如果需要配置多个入口,可以把 entry 定义成一个对象。entry 是定义为对象而不是数组,如果是数组的话就是把多个文件打包到一起,还是一个入口。这个对象中一个属性就是一个入口,属性名称就是这个入口的名称,值就是这个入口对应的文件路径。输出文件名 - 使用 [name] 这种占位符来输出动态的文件名 - [name]最终会被替换为入口的名称通过 html-webpack-plugin - 分别为 index 和 album 页面生成了对应的 HTML 文件分包加载输出 HTML 的插件,默认这个插件会自动注入所有的打包结果。如果需要指定所使用的 bundle,通过 HtmlWebpackPlugin 的 chunks 属性来设置每个打包入口都会形成一个独立的 chunk(块)// ./webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { entry: { index: './src/index.js', album: './src/album.js' }, output: { filename: '[name].bundle.js' // [name] 是入口名称 }, // ... 其他配置 plugins: [ new HtmlWebpackPlugin({ title: 'Multi Entry', template: './src/index.html', filename: 'index.html', chunks: ['index'] // 指定使用 index.bundle.js }), new HtmlWebpackPlugin({ title: 'Multi Entry', template: './src/album.html', filename: 'album.html', chunks: ['album'] // 指定使用 album.bundle.js }) ] } 复制代码提取公共模块需要把这些公共的模块提取到一个单独的 bundle 中优化配置中开启 splitChunks 功能// ./webpack.config.js module.exports = { entry: { index: './src/index.js', album: './src/album.js' }, output: { filename: '[name].bundle.js' // [name] 是入口名称 }, optimization: { splitChunks: { // 自动提取所有公共模块到单独 bundle chunks: 'all' } } // ... 其他配置 } 复制代码将它设置为 all,表示所有公共模块都可以被提取动态导入Code Splitting 更常见的实现方式还是结合 ES Modules 的动态导入特性,从而实现按需加载。一般我们常说的按需加载指的是加载数据或者加载图片,这里所说的按需加载,指的是在应用运行过程中,需要某个资源模块时,才去加载这个模块。极大地降低了应用启动时需要加载的资源体积提高了应用的响应速度节省了带宽和流量Webpack 中支持使用动态导入的方式实现模块的按需加载,而且所有动态导入的模块都会被自动提取到单独的 bundle 中,从而实现分包├── src │ ├── album │ │ ├── album.css │ │ └── album.js │ ├── common │ │ ├── fetch.js │ │ └── global.css │ ├── posts │ │ ├── posts.css │ │ └── posts.js │ ├── index.html │ └── index.js ├── package.json └── webpack.config.js 复制代码文章列表对应的是这里的 posts 组件,而相册列表对应的是 album 组件在打包入口(index.js)中同时导入了这两个模块,然后根据页面锚点的变化决定显示哪个组件// ./src/index.js // import posts from './posts/posts' // import album from './album/album' const update = () => { const hash = window.location.hash || '#posts' const mainElement = document.querySelector('.main') mainElement.innerHTML = '' if (hash === '#posts') { // mainElement.appendChild(posts()) import('./posts/posts').then(({ default: posts }) => { mainElement.appendChild(posts()) }) } else if (hash === '#album') { // mainElement.appendChild(album()) import('./album/album').then(({ default: album }) => { mainElement.appendChild(album()) }) } } window.addEventListener('hashchange', update) update() 复制代码为了动态导入模块,可以将 import 关键字作为函数调用。当以这种方式使用时,import 函数返回一个 Promise 对象.在需要使用组件的地方通过 import 函数导入指定路径方法返回的是一个 PromisePromise 的 then 方法中能够拿到模块对象由于这里的 posts 和 album 模块是以默认成员导出,需要解构模块对象中的 default,先拿到导出成员,然后再正常使用这个导出成员。import('./album/album').then(({ default: album }) => { mainElement.appendChild(album()) }) 复制代码魔法注释默认通过动态导入产生的 bundle 文件,它的 name 就是一个序号。如果需要给这些 bundle 命名的话,就可以使用 Webpack 所特有的魔法注释去实现import(/* webpackChunkName: 'posts' */'./posts/posts') .then(({ default: posts }) => { mainElement.appendChild(posts()) }) 复制代码所谓魔法注释,就是在 import 函数的形式参数位置,添加一个行内注释,注释有一个特定的格式---webpackChunkName:’xxx‘,就可以给分包的 chunk 起名字如果 chunkName 相同的话,那相同的 chunkName 最终就会被打包到一起,借助这个特点,就可以根据自己的实际情况,灵活组织动态加载的模块了。Tree ShakingTree-shaking 最早是 Rollup 中推出的一个特性,Webpack 从 2.0 过后开始支持这个特性。使用 Webpack 生产模式打包的优化过程中,自动开启这个功能 --- npx webpack --mode=production其他模式开启 Tree Shaking配置对象中添加一个 optimization 属性,该属性用来集中配置 Webpack 内置优化功能,它的值也是一个对象,在 optimization 对象中先开启一个 usedExports 选项,表示在输出结果中只导出外部使用了的成员module.exports = { // ... 其他配置项 optimization: { // 模块只导出被使用的成员 usedExports: true } } 复制代码对于未引用代码,如果我们开启压缩代码功能,就可以自动压缩掉这些没有用到的代码.module.exports = { // ... 其他配置项 optimization: { // 模块只导出被使用的成员 usedExports: true, // 压缩输出结果 minimize: true } } 复制代码Tree-shaking 的实现,整个过程用到了 Webpack 的两个优化功能usedExports打包结果中只导出外部用到的成员minimize压缩打包结果把代码看成一棵大树usedExports的作用就是标记树上哪些是枯树枝、枯树叶minimize 的作用就是负责把枯树枝、枯树叶摇结合 babel-loader 的问题Tree-shaking 实现的前提是 ES Modules,最终交给 Webpack 打包的代码,必须是使用 ES Modules 的方式来组织的模块化Webpack 在打包所有的模块代码之前先是将模块根据配置交给不同的 Loader 处理最后再将 Loader 处理的结果打包到一起为了更好的兼容性,会选择使用 babel-loader 去转换我们源代码中的一些 ECMAScript 的新特性,Babel 在转换 JS 代码时,很有可能处理掉代码中的 ES Modules 部分,把它们转换成 CommonJS 的方式。babel-loader (低版本)我们为 Babel 配置的都是一个 preset(预设插件集合),而不是某些具体的插件。目前市面上使用最多的 @babel/preset-env,这个预设里面就有转换 ES Modules 的插件。使用这个预设时,代码中的 ES Modules 部分就会被转换成 CommonJS 方式。Webpack 再去打包时,拿到的就是以 CommonJS 方式组织的代码了,所以 Tree-shaking 不能生效module.exports = { module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env'] ] } } } ] }, optimization: { usedExports: true } } 复制代码最新版本(8.x)的 babel-loader自动帮我们关闭了对 ES Modules 转换的插件,经过 babel-loader 处理后的代码默认仍然是 ES Modules。那 Webpack 最终打包得到的还是 ES Modules 代码。Tree-shaking 自然也就可以正常工作了最新版本的 babel-loader 并不会导致 Tree-shaking 失效,确保babel-loader能使用Tree-shaking。最简单的办法就是在配置中将 @babel/preset-env 的 modules 属性设置为 false。确保不会转换 ES Modules,也就确保了 Tree-shaking 的前提module.exports = { module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env', { modules: 'false' }] ] } } } ] }, optimization: { usedExports: true } } 复制代码Scope Hoisting (作用域提升)Webpack 3.0 中添加的一个特性,使用 concatenateModules 选项继续优化输出普通打包只是将一个模块最终放入一个单独的函数中,如果模块很多,就意味着在输出结果中会有很多的模块函数。concatenateModules 配置的作用,尽可能将所有模块合并到一起输出到一个函数中,既提升了运行效率,又减少了代码的体积。module.exports = { // ... 其他配置项 optimization: { // 模块只导出被使用的成员 usedExports: true, // 尽可能合并每一个模块到一个函数中 concatenateModules: true, } } 复制代码bundle.js 中就不再是一个模块对应一个函数了,而是把所有的模块都放到了一个函数中sideEffectsWebpack 4 中新增了一个 sideEffects 特性,允许通过配置标识我们的代码是否有副作用,从而提供更大的压缩空间。模块的副作用指的就是模块执行的时候除了导出成员,是否还做了其他的事情,特性一般只有去开发一个 npm 模块时才会用到。Tree-shaking 只能移除没有用到的代码成员,而想要完整移除没有用到的模块,那就需要开启 sideEffects 特性了,在 optimization 中开启 sideEffects 特性// ./webpack.config.jsmodule.exports = { mode: 'none', optimization: { sideEffects: true } } 复制代码这个特性在 production 模式下同样会自动开启Webpack 缓存优化利用缓存数据来加速构建过程的处理。在初次构建的压缩代码过程中,就将这一阶段的结果写入了缓存目录(node_modules/.cache/插件名/)中有缓存。当再次构建进行到压缩代码阶段时,即可对比读取已有缓存。编译阶段的缓存优化优化打包阶段的缓存优化编译阶段的缓存优化编译过程的耗时点主要在使用不同加载器(Loader)来编译模块的过程Babel-loaderBabel-loader 是绝大部分项目中会使用到的 JS/JSX/TS 编译器与缓存相关的设置主要有cacheDirectory默认为 false,即不开启缓存当值为true时开启缓存并使用默认缓存目录./node_modules/.cache/babel-loader/也可以指定其他路径值作为缓存目录cacheIdentifier用于计算缓存标识符默认使用Babel 相关依赖包的版本babelrc 配置文件的内容环境变量与模块内容一起参与计算缓存标识符cacheCompression默认为 true将缓存内容压缩为 gz 包以减小缓存目录的体积在设为 false 的情况下将跳过压缩和解压的过程,从而提升这一阶段的速度Cache-loader在编译过程中利用缓存的第二种方式是使用 --- Cache-loader在使用时,需要将 cache-loader 添加到对构建效率影响较大的 Loader(如 babel-loader 等)之前module: { rules: [ { test: /\.js$/, use: ['cache-loader', 'babel-loader'], }, ], } 复制代码使用 cache-loader 后,比使用 babel-loader 的开启缓存选项后的构建时间更短主要原因是 babel-loader 中的缓存信息较少,而 cache-loader 中存储的 Buffer 形式的数据处理效率更高。优化打包阶段的缓存优化生成 ChunkAsset 时的缓存优化在 Webpack 4 中,生成 ChunkAsset 过程中的缓存优化是受限制的:只有在 watch 模式下且配置中开启 cache 时(development 模式下自动开启),才能在这一阶段执行缓存的逻辑在 Webpack 4 中,缓存插件是基于内存的,只有在 watch 模式下才能在内存中获取到相应的缓存数据对象代码压缩时的缓存优化对于 JS 的压缩TerserWebpackPlugin/UglifyJSPlugin都是支持缓存设置的。对于 CSS 的压缩,目前最新发布的 CSSMinimizerWebpackPlugin 支持且默认开启缓存,其他的插件如 OptimizeCSSAssetsPlugin 和 OptimizeCSSNanoPlugin 目前还不支持使用缓存使用缓存注意点如何最大程度地让缓存命中,成为我们选择缓存方案后首先要考虑的缓存标识符发生变化导致的缓存失效,支持缓存的 Loader 和插件中,会根据一些固定字段的值加上所处理的模块或 Chunk 的数据 hash 值来生成对应缓存的标识符。一旦其中的值发生变化,对应缓存标识符就会发生改变,意味着对应工具中,所有之前的缓存都将失效。需要尽可能少地变更会影响到缓存标识符生成的字段优化打包阶段的缓存失效,尽管在模块编译阶段每个模块是单独执行编译的。但是当进入到代码压缩环节时,各模块已经被组织到了相关联的 Chunk 中,N个模块最后只生成了一个 Chunk。任何一个模块发生变化都会导致整个 Chunk 的内容发生变化,而使之前保存的缓存失效。优化方案尽可能地把那些不变的处理成本高昂的模块打入单独的 Chunk 中,Webpack 中的分包配置——splitChunks。使用 splitChunks 优化缓存利用率。好处合并通用依赖提升构建缓存利用率提升资源访问的缓存利用率资源懒加载CI/CD 中的缓存目录问题自动化集成的系统中,项目的构建空间会在每次构建执行完毕后,立即回收清理。在这种情况下,默认的项目构建缓存目录(node_mo dules/.cache)将无法留存。导致即使项目中开启了缓存设置,也无法享受缓存的便利性,反而因为需要写入缓存文件而浪费额外的时间如果需要使用缓存,则需要根据对应平台的规范,将缓存设置到公共缓存目录下缓存的便利性本质在于用磁盘空间换取构建时间,需要考虑对缓存区域的定期清理后记分享是一种态度。参考资料:效率工程化Webpack官网全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。
我们每个人来到这个世上,都有着自己注定要完成的使命;但是,我们不知道我们的使命是什么。所以,我们需要在前进的路上,不断地去反思,不停地去理解。只有当我们明白了我们的使命,明白了我们奋斗的意义,我们才不会在众多的岔路中迷失方向,才会走的更加坚定。 --《心理学通识》前言大家好,我是柒八九。因为,最近有一个需求中,用到了Worker技术,然后经过一些调研和调试,成功的在项目中应用。虽然,有部分原因是出于技术尝鲜的角度才选择Worker进行性能优化。但是,看懂了,会用了,领悟了。这是不同的技术层面。所以,打算做一个Worker科普和实际生产应用的文章。那我们就闲话少叙,开车走起。 文章概要Worker 线程简介{专用工作线程|Dedicated Worker}专用工作线程 + Webpack{共享工作线程| Shared Workers }Worker 线程简介JavaScript 环境实际上是运行在托管操作系统(OS)中的虚拟环境在浏览器中每打开一个页面,就会分配一个它自己的环境:即每个页面都有自己的内存、事件循环、DOM。并且每个页面就相当于一个沙盒,不会干扰其他页面。而使用Worker 线程,浏览器可以在原始页面环境之外再分配一个完全独立的二级子环境。这个子环境不能与依赖单线程交互的 API(如 DOM)互操作,但可以与父环境并行执行代码。1. Worker线程 vs 线程共同之处工作者线程是以实际线程实现的:Blink 浏览器引擎实现Worker线程的 WorkerThread 就对应着底层的线程工作者线程并行执行:虽然页面和工作者线程都是单线程 JS 环境,每个环境中的指令则可以并行执行工作者线程可以共享某些内存:工作者线程能够使用 SharedArrayBuffer 在多个环境间共享内容区别worker线程不共享全部内存:除了 SharedArrayBuffer 外,从工作者线程进出的数据需要复制或转移worker线程不一定在同一个进程里:例如,Chrome 的 Blink 引擎对共享worker 线程和服务worker线程使用独立的进程创建worker线程的开销更大:工作者线程有自己独立的事件循环、全局对象、事件处理程序和其他 JS 环境必需的特性。创建这些结构的代价不容忽视2. Worker的类型Worker 线程规范中定义了三种主要的工作者线程{专用工作线程|Dedicated Web Worker} 专用工作者线程,通常简称为工作者线程、Web Worker 或 Worker,是一种实用的工具,可以让脚本单独创建一个 JS 线程,以执行委托的任务。只能被创建它的页面使用{共享工作线程|Shared Web Worker} :共享工作者线程可以被多个不同的上下文使用,包括不同的页面。任何与创建共享工作者线程的脚本同源的脚本,都可以向共享工作者线程发送消息或从中接收消息{服务工作线程|Service Worker}:主要用途是拦截、重定向和修改页面发出的请求,充当网络请求的仲裁者的角色3. WorkerGlobalScope在网页上,window 对象可以向运行在其中的脚本暴露各种全局变量。在Worker线程内部,没有 window 的概念全局对象是 WorkerGlobalScope 的实例,通过 self 关键字暴露出来WorkerGlobalScope 属性WorkerGlobalScope 方法self 上可用的属性/方法是 window 对象上属性/方法的严格子集2. {专用工作线程|Dedicated Web Worker}专用工作线程是最简单的 Web 工作者线程,网页中的脚本可以创建专用工作者线程来执行在页面线程之外的其他任务。这样的线程可以与父页面交换信息、发送网络请求、执行文件输入/输出、进行密集计算、处理大量数据,以及实现其他不适合在页面执行线程里做的任务(否则会导致页面响应迟钝)。其实,我们平时在工作中,遇到的最多的也是专用工作线程。基本概念把专用工作线程称为{后台脚本|background script}JS 线程的各个方面,包括生命周期管理、代码路径和输入/输出,都由初始化线程时提供的脚本来控制。创建工作线程创建工作线程最常见的方式是加载 JS 文件:即把文件路径提供给 Worker构造函数,然后构造函数再在后台异步加载脚本并实例化工作线程。worker.js // 进行密集计算 bala bala main.js const worker = new Worker( 'worker.js'); console.log(worker); // Worker {} // {3} 复制代码这里有几个点需要注意下:这个文件(worker.js)是在后台加载的,工作线程的初始化完全独立于 main.js工作线程本身存在于一个独立的 JS 环境中,因此 main.js 必须以 Worker 对象 为代理实现与工作线程通信在{3}行,虽然相应的工作线程可能还不存在,但该 Worker 对象已在原始环境中可用了安全限制工作线程的脚本文件只能从与父页面相同的源加载。从其他源加载工作线程的脚本文件会导致错误,如下所示:假设父页面为https://bcnz.com // 尝试基于 与父页面同源的脚本创建工作者线程 const sameOriginWorker = new Worker('./worker.js'); // 尝试基于 https://wl.com/worker.js 创建工作者线程 (与父页面不同源) const remoteOriginWorker = new Worker('https://wl.com/worker.js'); 复制代码在创建remoteOriginWorker时,页面报错。// Error: Uncaught DOMException: // Failed to construct 'Worker': // Script at https://wl.com/main.js cannot be accessed // from origin https://bcnz.com 复制代码不能使用非同源脚本创建工作线程,并不影响执行其他源的脚本使用 Worker 对象Worker()构造函数返回的 Worker 对象是与刚创建的专用工作线程通信的连接点Worker 对象可用于在工作线程和父上下文间传输信息,以及捕获专用工作线程发出的事件。Worker 对象支持下列事件处理程序属性:onerror:在工作线程中发生ErrorEvent类型的错误事件时会调用指定给该属性的处理程序该事件会在工作线程中抛出错误时发生该事件也可以通过 worker.addEventListener('error', handler)的形式处理onmessage:在工作线程中发生MessageEvent类型的消息事件时会调用指定给该属性的处理程序该事件会在工作线程向父上下文发送消息时发生该事件也可以通过使用 worker.addEventListener('message', handler)处理onmessageerror:在工作线程中发生MessageEvent类型的错误事件时会调用指定给该属性的处理程序该事件会在工作线程收到无法反序列化的消息时发生该事件也可以通过使用 worker.addEventListener('messageerror', handler)处理Worker 对象还支持下列方法postMessage():用于通过异步消息事件向工作线程发送信息。terminate():用于立即终止工作线程。没有为工作线程提供清理的机会,脚本会突然停止DedicatedWorkerGlobalScope在专用工作线程内部,全局作用域是 DedicatedWorkerGlobalScope 的实例。因为这继承自 WorkerGlobalScope,所以包含它的所有属性和方法。工作线程可以通过 self 关键字访问该全局作用域。globalScopeWorker.js console.log('inside worker:', self); main.js const worker = new Worker('./globalScopeWorker.js'); console.log('created worker:', worker); // created worker: Worker {} // inside worker: DedicatedWorkerGlobalScope {} 复制代码两个独立的 JS 线程都在向一个 console 对象发消息,该对象随后将消息序列化并在浏览器控制台打印出来。浏览器从两个不同的 JS 线程收到消息,并按照自己认为合适的顺序输出这些消息。DedicatedWorkerGlobalScope 在 WorkerGlobalScope 基础上增加了以下属性和方法name:可以提供给 Worker 构造函数的一个可选的字符串标识符。postMessage():与 worker.postMessage()对应的方法,用于从工作线程内部向父上下文发送消息close():与 worker.terminate()对应的方法,用于立即终止工作者线程。没有为工作者线程提供清理的机会,脚本会突然停止importScripts():用于向工作线程中导入任意数量的脚本生命周期调用 Worker()构造函数是一个专用工作线程生命的起点调用之后,它会初始化对工作线程脚本的请求,并把 Worker 对象返回给父上下文。虽然父上下文中可以立即使用这个 Worker 对象,但与之关联的工作线程可能还没有创建,因为存在请求脚本的网格延迟和初始化延迟。一般来说,专用工作线程可以非正式区分为处于下列三个状态:{初始化|initializing}、{激活|active} 和{终止|terminated}。这几个状态对其他上下文是不可见的。虽然 Worker 对象可能会存在于父上下文 中,但也无法通过它确定工作者线程当前是处理初始化、活动还是终止状态。初始化时,虽然工作线程脚本尚未执行,但可以先把要发送给工作线程的消息加入队列。这些消息会等待工作线程的状态变为活动,再把消息添加到它的消息队列。initializingWorker.js self.addEventListener('message', ({data}) => console.log(data)); main.js const worker = new Worker('./initializingWorker.js'); // Worker 可能仍处于初始化状态 // 但 postMessage()数据可以正常处理 worker.postMessage('foo'); worker.postMessage('bar'); worker.postMessage('baz'); // foo // bar // baz 复制代码可以看到,在主线程中,创建了对应工作线程对应的 Worker 对象,在还未知道工作线程是否已经初始化完成,便可以直接通过postMessage进行线程之间通信。创建之后,专用工作线程就会伴随页面的整个生命期而存在,除非自我终止(self.close()) 或通过外部终止(worker.terminate())。即使线程脚本已运行完成,线程的环境仍会存在。只要工作线程仍存在,与之关联的 Worker 对象就不会被当成垃圾收集掉在整个生命周期中,一个专用工作线程只会关联一个网页(也称文档)。除非明确终止,否则只要关联文档存在,专用工作线程就会存在。Worker 选项Worker()构造函数允许将可选的配置对象作为第二个参数。name:可以在工作线程中通过 self.name 读取到的字符串标识符。type:表示加载脚本的运行方式,可以是classic或module。classic 将脚本作为常规脚本来执行module 将脚本作为模块来执行credentials:在 type 为module时,指定如何获取与传输凭证数据相关的工作线程模块脚本。值可以是omit、same-orign或include。这些选项与 fetch()的凭证选项相同。行内创建工作线程基于Blob专用工作线程也可以基于 Blob 实例创建 URL 对象 在行内脚本创建。// 创建要执行的 JavaScript 代码字符串 const workerScript = ` self.onmessage = ({data}) => console.log(data); `; // 基于脚本字符串生成 Blob 对象 const workerScriptBlob = new Blob([workerScript]); // 基于 Blob 实例创建对象 URL const workerScriptBlobUrl = URL.createObjectURL(workerScriptBlob); // 基于对象 URL 创建专用工作者线程 const worker = new Worker(workerScriptBlobUrl); worker.postMessage('blob worker script'); // blob worker script 复制代码通过脚本字符串创建了 Blob然后又通过 Blob 创建了 URL 对象最后把URL 对象,传给了 Worker()构造函数基于函数序列化函数的 toString()方法返回函数代码的字符串,而函数可以在父上下文中定义但在子上下文中执行function fibonacci(n) { return n < 1 ? 0 : n <= 2 ? 1 : fibonacci(n - 1) + fibonacci(n - 2); } const workerScript = ` self.postMessage( (${fibonacci.toString()})(9) ); `; const worker = new Worker(URL.createObjectURL(new Blob([workerScript]))); worker.onmessage = ({data}) => console.log(data); // 34 复制代码像这样序列化函数有个前提,就是函数体内不能使用通过闭包获得的引用,也包括全局变量。动态执行脚本工作线程可以使用 importScripts()方法通过编程方式加载和执行任意脚本这个方法会加载脚本并按照加载顺序同步执行。// Worker.jsscriptA.js console.log('scriptA executes'); scriptB.js console.log('scriptB executes'); worker.js console.log('importing scripts'); importScripts('./scriptA.js'); importScripts('./scriptB.js'); console.log('scripts imported'); 复制代码Main.jsconst worker = new Worker('./worker.js'); // importing scripts // scriptA executes // scriptB executes // scripts imported 复制代码importScripts()方法可以接收任意数量的脚本作为参数。执行会严格按照它们在参数列表的顺序进行。脚本加载受到常规 CORS 的限制,但在工作线程内部可以请求来自任何源的脚本在这种情况下,所有导入的脚本也会共享作用域。Worker.jsscriptA.js console.log(`scriptA executes in ${self.name} with ${globalToken}`); scriptB.js console.log(`scriptB executes in ${self.name} with ${globalToken}`); worker.js const globalToken = 'wl'; console.log(`importing scripts in ${self.name} with ${globalToken}`); importScripts('./scriptA.js', './scriptB.js'); console.log('scripts imported'); 复制代码main.jsconst worker = new Worker('./worker.js', {name: 'foo'}); // importing scripts in foo with wl // scriptA executes in foo with wl // scriptB executes in foo with wl // scripts imported 复制代码与专用工作线程通信与工作线程的通信都是通过异步消息完成的使用 postMessage()是使用 postMessage()传递序列化的消息。factorialWorker.jsfunction factorial(n) { let result = 1; while(n) { result *= n--; } return result; } self.onmessage = ({data}) => { self.postMessage(`${data}! = ${factorial(data)}`); }; 复制代码main.jsconst factorialWorker = new Worker('./factorialWorker.js'); factorialWorker.onmessage = ({data}) => console.log(data); // 发送消息 factorialWorker.postMessage(5); factorialWorker.postMessage(7); factorialWorker.postMessage(10); // 5! = 120 // 7! = 5040 // 10! = 3628800 复制代码对于传递简单的消息,使用 postMessage()在主线程和工作者线程之间传递消息。并且没有 targetOrigin 的限制。然后还可以使用MessageChannel/BroadcastChannel进行线程之间的通信,这里就不展开说明了。但是大部分,用postMessage()就够用了数据传输工作线程是独立的上下文,因此在上下文之间传输数据就会产生消耗。在 JS 中,有三种在上下文间转移信息的方式:{结构化克隆算法|structured clone algorithm}、{可转移对象| transferable objects }{共享数组缓冲区| shared array buffers}结构化克隆算法结构化克隆算法可用于在两个独立上下文间共享数据在通过 postMessage()传递对象时,浏览器会遍历该对象,并在目标上下文中生成它的一个副本。结构化克隆算法支持的类型需要注意的点结构化克隆算法在对象比较复杂时会存在计算性消耗。因此,实践中要尽可能避免过大、过多的复制。可转移对象使用可转移对象可以把所有权从一个上下文转移到另一个上下文。在不太可能在上下文间复制大量数据的情况下,这个功能特别有用。可转移对象支持的类型ArrayBufferMessagePortImageBitmapOffscreenCanvaspostMessage()方法的第二个可选参数是数组,它指定应该将哪些对象转移到目标上下文。在遍历消息负载对象时,浏览器根据转移对象数组检查对象引用,并对转移对象进行转移而不复制它们。把 ArrayBuffer 指定为可转移对象,那么对缓冲区内存的引用就会从父上下文中抹去,然后 分配给工作者线程。main.jsconst worker = new Worker('./worker.js'); // 创建 32 位缓冲区 const arrayBuffer = new ArrayBuffer(32); console.log(`page's buffer size: ${arrayBuffer.byteLength}`); // 32 worker.postMessage(arrayBuffer, [arrayBuffer]); console.log(`page's buffer size: ${arrayBuffer.byteLength}`); // 0 复制代码worker.jsself.onmessage = ({data}) => { console.log(`worker's buffer size: ${data.byteLength}`); // 32 }; 复制代码共享数组缓冲区既不克隆,也不转移,SharedArrayBuffer 作为 ArrayBuffer 能够在不同浏览器上下文间共享。在把 SharedArrayBuffer 传给 postMessage()时,浏览器只会传递原始缓冲区的引用。结果是,两个不同的 js 上下文会分别维护对同一个内存块的引用。专用工作线程 + Webpack假设存在如下的一种文档结构package.json src/ app.jsx // 组件 work/ longTime.js // 计算耗时任务 store/ webpack/ config.js babel.config.js .gitignore README.md 复制代码行内创建工作线程就像上面介绍的一样,我们可以借用行内方式来创建一个工作线程来,维护一些比较耗时的操作。在longTime.js 中注入一些耗时任务const workercode = () => { self.onmessage = function (e) { console.log('来自主线程的消息'); let workerResult = `主线程消息: ${e.data}`; console.log('向主线程回传消息'); self.postMessage(workerResult); }; self.postMessage('老表,你好!'); }; // 将函数进行序列化处理(toString()) let code = workercode.toString(); // 将函数体,用{} 包裹起来 code = code.substring( code.indexOf('{') + 1, code.lastIndexOf('}')); const blob = new Blob( [code], { type: 'application/javascript' } ); const worker_script = URL.createObjectURL(blob); export default worker_script; 复制代码import worker_script from './longTime.js'; const { useEffect } from 'react'; const MainPage = () => { useEffect(()=>{ const worker = new Worker(worker_script); worker.onmessage = function (event) { console.log(`Received message ${event.data}`); }; // worker.postMessage('dadada') },[]); return <>页面内容</> } 复制代码当然,我们可以在利用useRef()来引用worker 引用,然后再其他副作用或者事件函数中触发,worker.postMessage('')worker 引用node_module中的包通过行内构建工作线程有一个弊端,就是无法通过import/require引入一些第三方的包。因为,前端框架的特殊性,虽然在worker中可以使用importScripts()加载任意脚本,但是那些都是在worker同目录或者是利用绝对路径进行引用。很不方便。而大部分前端项目,都是用node_module对项目用到的包进行管理。所以,利用importScripts()这种方式引入包,不满足情况。既然,不满足,我们就需要将目光移打包框架层面。Webpack作为打包界的扛把子。我们还是需要问问它是否满足这种情况。巧不了不是,还真有一些类似的loader --worker-loader进行本地按照$ npm install worker-loader --save-dev配置webpack -config.jsmodule.exports = { module: { rules: [ { test: /\.worker\.js$/, use: { loader: "worker-loader" }, }, ], }, }; 复制代码通过如上的配置,我们就可以像写常规的组件或者工具方法一些,肆无忌惮的通过import引入第三方包。longTime.jsconst A = require('A') self.onmessage = function (e) { // A处理一些特殊场景 } 复制代码关于worker-loader具体使用规范就不在过多解释。{共享工作线程| Shared Workers }从行为上讲,共享工作线程可以看作是专用工作线程的一个扩展。线程创建、线程选项、安全限制和 importScripts()的行为都是相同的。共享工作者线程也在独立执行上下文中运行,也只能与其他上下文异步通信。因为,Shared Worker简单也适用场景有限,所以就不过多介绍了。关于服务线程其实可涉及的地方还有很多,打算单写一篇。在这里就不单独介绍了。后记分享是一种态度。全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。
人生就是受苦受难。但勇者不懈抗争,最终主宰自我—— 拿破仑前言在上一篇,网络通信之生成HTTP消息中我们介绍了,针对浏览器如何生成 HTTP 消息,并且通过 HTTP 消息进行与服务器之间进行数据交互。我们是从上帝视角对该过程进行了描述,忽略了很多具体细节。而接下来,我们来讲讲和网络通信密不可分的一个部分:IP地址。简明扼要IP 地址是一个网卡在网络世界的通讯地址,相当于我们现实世界的门牌号码lo 全称是loopback,又称环回接口,往往会被分配到 127.0.0.1 这个地址。这个地址用于本机通信,经过内核处理后直接返回,不会在任何网络中出现对于 A、B、 C 类主要分两部分,前面一部分是网络号,后面一部分是主机号。IP 地址的主机号全 0:表示整个子网全 1:表示向子网上所有设备发送包,即“广播"一个网络包要从一个地方传到另一个地方,除了要有确定的地址,还需要有定位功能MAC 地址更像是身份证,是一个唯一的标识只要是在网络上跑的包,都是完整的,可以有下层没上层,绝对不可能有上层没下层。文章概要IP地址是个啥?无类型域间选路(CIDR)公有 IP 地址和私有 IP 地址MAC 地址动态主机配置协议(DHCP)1. IP地址是个啥?客户端(浏览器)生成 HTTP 消息之后,接下来需要委托操作系统将消息发送给 Web 服务器。在委托操作系统发送消息时,必须要提供的不是通信对象的域名,而是它的 IP 地址。我们可以通过一些命令,来查看本机的IP地址。在 Windows 上是 ipconfig,在 Linux 上是 ifconfig/ip addr。我在自己的电脑中,搭建了一个Linux环境,那就按ip addr执行查看IP命令。命令显示了这台机器上所有的网卡。大部分的网卡都会有一个 IP 地址。IP 地址是一个网卡在网络世界的通讯地址,相当于我们现实世界的门牌号码127.0.0.1 就是一个 IP 地址。这个地址被点分隔为四个部分,每个部分 8 个 bit,所以 IP 地址总共是 32 位。这样产生的 IP 地址的数量很快就不够用了。于是就有了 有128 位的IPv6。这里有一个小的知识点在 IP 地址的后面有个 scope,如果值是 global,说明这张网卡是可以对外的,可以接收来自各个地方的包。对于 lo 来讲,是 host,说明这张网卡仅仅可以供本机相互通信。lo 全称是loopback,又称环回接口,往往会被分配到 127.0.0.1 这个地址。这个地址用于本机通信,经过内核处理后直接返回,不会在任何网络中出现本来 32 位的 IP 地址就不够,还被分成了 5 类。对于 A、B、 C 类主要分两部分,前面一部分是网络号,后面一部分是主机号。相当于现实中某条路上的“×× 号 ×× 室”。其中“号”对应的号码是分配给整个子网的,而“室”对应的号码是分配给子网中的计算机的,这就是网络中的地址。“号”对应的号码称为网络号“室”对应的号码称为主机号这个地址的整体称为 IP 地址。C 类地址能包含的最大主机数量实在太少了,只有 254 个。而 B 类地址能包含的最大主机数量又太多了。6 万多台机器放在一个网络下面。无类型域间选路(CIDR)无类型域间选路,简称CIDR。这种方式打破了原来设计的几类地址的做法,将 32 位的 IP 地址一分为二,前面是网络号,后面是主机号。10.11.12.13/24,这个 IP 地址中有一个斜杠,斜杠后面有个数字 24。这种地址表示形式,就是 CIDR。后面 24 的意思是,32 位中,前 24 位是网络号,后 8 位是主机号。伴随着 CIDR 存在的,一个是广播地址,10.11.12.255。如果发送这个地址,所有 10.11.12 网络里面的机器都可以收到。另一个是子网掩码,255.255.255.0。IP 地址的主机号全 0:表示整个子网全 1:表示向子网上所有设备发送包,即“广播"3. 公有 IP 地址和私有 IP 地址表格最右列是私有 IP 地址段。平时我们看到的数据中心里,办公室、家里或学校的 IP 地址,一般都是私有 IP 地址段。因为这些地址允许组织内部的 IT 人员自己管理、自己分配,而且可以重复。因此,你学校的某个私有 IP 地址段和我学校的可以是一样的。公有 IP 地址有个组织统一分配,你需要去买。表格中的 192.168.0.x 是最常用的私有 IP 地址。针对192.168.0.x/24不需要将十进制转换为二进制 32 位,就能明显看出 192.168.0 是网络号,后面是主机号。而整个网络里面的第一个地址192.168.0.1,往往就是你这个私有网络的出口地址。例如,你家里的电脑连接 Wi-Fi,Wi-Fi 路由器的地址就是 192.168.0.1,而 192.168.0.255 就是广播地址。一旦发送这个地址,整个 192.168.0 网络里面的所有机器都能收到。4. MAC 地址在 IP 地址的上一行是 link/ether xxxx,这个被称为MAC 地址,是一个网卡的物理地址,用十六进制,6 个 byte 表示。MAC 地址全局唯一,不会有两个网卡有相同的 MAC 地址,而且网卡自生产出来,就带着这个地址。很多人看到这里就会想,只要知道了对方的 MAC 地址,就可以把信息传过去。这是行不通的。一个网络包要从一个地方传到另一个地方,除了要有确定的地址,还需要有定位功能。 而有门牌号码属性的 IP 地址,才是有远程定位功能的。MAC 地址更像是身份证,是一个唯一的标识。它的唯一性设计是为了组网的时候,不同的网卡放在一个网络里面的时候,可以不用担心冲突。从硬件角度,保证不同的网卡有不同的标识。MAC 地址是有一定定位功能的,只不过范围非常有限。MAC 地址的通信范围比较小,局限在一个子网里面5. 动态主机配置协议(DHCP)只要是在网络上跑的包,都是完整的,可以有下层没上层,绝对不可能有上层没下层。假设,我们想要在源 IP 地址为 16.158.23.6 的机器上访问目标 IP 地址 为 192.168.1.6机器的数据。发现包发不出去,这是因为 MAC 层还没填。自己的 MAC 地址自己知道。但是目标 MAC 填什么呢?是不是填 192.168.1.6 这台机器的 MAC 地址呢?当然不是。Linux 首先会判断,要去的这个地址和我是一个网段的吗,或者和我的一个网卡是同一网段的吗?只有是一个网段的,它才会发送 ARP 请求,获取 MAC 地址。如果发现不是,Linux 默认的逻辑是,如果这是一个跨网段的调用,它便不会直接将包发送到网络上,而是企图将包发送到网关。如果你配置了网关的话,Linux 会获取网关的 MAC 地址,然后将包发出去。对于 192.168.1.6 这台机器来讲,虽然路过它家门的这个包,目标 IP 是它,但是无奈 MAC 地址不是它的,所以它的网卡是不会把包收进去的。手动配置需要不停的对自己机器的IP进行配置。是一件很麻烦的事。需要有一个自动配置的协议,也就是称动态主机配置协议(Dynamic Host Configuration Protocol),简称DHCP。只需要配置一段共享的 IP 地址。每一台新接入的机器都通过 DHCP 协议,来这个共享的 IP 地址里申请,然后自动配置好就可以了。等人走了,或者用完了,还回去,这样其他的机器也能用。解析 DHCP 的工作方式当一台机器新加入一个网络的时候,只知道自己的 MAC 地址。这时候的沟通基本靠“吼”。这一步,我们称为DHCP Discover。新来的机器使用 IP 地址 0.0.0.0 发送了一个广播包,目的 IP 地址为 255.255.255.255。广播包封装了 UDP,UDP 封装了 BOOTP(Bootstrap Protocol,引导程序协议)。其实 DHCP 是 BOOTP 的增强版。在这个广播包里面,新人大声喊:我是新来的(Boot request),我的 MAC 地址是这个,我还没有 IP,谁能给租给我个 IP 地址!如果在网络里面配置了DHCP Server的话,他就相当于这些 IP 的管理员。当一台机器带着自己的 MAC 地址加入一个网络的时候,MAC 是它唯一的身份,如果连这个都重复了,就没办法配置了。只有 MAC 唯一,IP 管理员才能知道这是一个新人,需要租给它一个 IP 地址,这个过程我们称为DHCP OfferDHCP Server 仍然使用广播地址作为目的地址。因为,此时请求分配 IP 的新人还没有自己的 IP。如果有多个 DHCP Server,这台新机器会收到多个 IP 地址,它会选择其中一个 DHCP Offer,一般是最先到达的那个,并且会向网络发送一个 DHCP Request 广播数据包,包中包含客户端的 MAC 地址、接受的租约中的 IP 地址、提供此租约的 DHCP 服务器地址等,并告诉所有 DHCP Server 它将接受哪一台服务器提供的 IP 地址,告诉其他 DHCP 服务器,并请求撤销它们提供的 IP 地址,以便提供给下一个 IP 租用请求者。由于还没有得到 DHCP Server 的最后确认,客户端仍然使用 0.0.0.0 为源 IP 地址、255.255.255.255 为目标地址进行广播。当 DHCP Server 接收到客户机的 DHCP request 之后,会广播返回给客户机一个 DHCP ACK 消息包,表明已经接受客户机的选择,并将这一 IP 地址的合法租用信息和其他的配置信息都放入该广播包,发给客户机。通过如上操作,一台新的机器就被分配了指定的IP地址。通过该IP地址,就可以和网络中的其他机器进行通信了。后记分享是一种态度,这篇文章,主要的篇幅来自于《趣谈网络协议》,算是一个自我学习过程中的一种记录和总结。主要是把自己认为重要的点,都罗列出来。同时,也是为大家节省一下排雷和踩坑的时间。当然,可能由于自己认知能力所限,有些点,没能表达的很好。如果大家想看原文,“墙裂推荐”看原文。参考资料:趣谈网络协议网络是如何连接的
概要浏览器架构总览进程、线程站点隔离渲染流程总览导航阶段UI进程👉拼装URL网络进程👉获取数据重定向根据Content-Type进行数据处理唤起渲染进程更新Tab状态 -->导航阶段结束渲染阶段编译处理BNFHTML 解析器DOM标记算法DOM树构建算法处理子资源将CSS附加(attachment)到DOM节点==>生成Render TreeCSS 解析器创建呈现器属性标准化样式计算并保存到ComputedStyle布局(Layout)绘制(Paint)-生成元素绘制顺序页面合成(Compositing)-->先分层,栅格化->页面合成分层栅格化(raster)操作前提概要在今年第一篇文章中,我们已经讲述过,我们以后的行文路线会按照前端 Roadmap 的进行。我们今天来简单介绍一下行文路径中 Browser and how they work - 浏览器是如何运行的。根据权威机构统计调查,常规的主流浏览器在全球范围内所在的比重如下图所示。所以,本文以Chrome浏览器来展示浏览器的工作流程。浏览器架构总览进程、线程在开始介绍浏览器工作流程的时候,我们需要简单说一下,进程、线程。进程:某个应用程序的执行程序。线程:常驻在进程内部并负责该进程部分功能的执行程序。当你启动一个应用程序,对应的进程就被创建。进程可能会创建一些线程用于帮助它完成部分工作,新建线程是一个可选操作。在启动某个进程的同时,操作系统(OS)也会分配内存以用于进程进行私有数据的存储。该内存空间是和其他进程是互不干扰的。当应用程序被关闭,进程也随之关闭,同时OS会将进程所占的内存释放掉。其实这是一个动图,由于掘金没法嵌入 svg 格式的图片,video 也不行。所以,如果想看流程可以通过 传送门进行了解。有人的地方就会有江湖,如果想让多人齐心协力的办好一件事,就需要一个人去统筹这些工作,然后通过大喇叭将每个人的诉求告诉对方。而对于计算机而言,统筹的工作归OS系统负责,OS通过Inter Process Communication (IPC)的机制去传递消息。其实这是一个动图,由于掘金没法嵌入 svg 格式的图片,video 也不行。所以,如果想看流程可以通过 传送门进行了解。下面,我们来看看Chrome架构是如何组织的。Chrome 的默认策略是,每个标签对应一个Render Process。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程。官方把这个默认策略叫 process-per-site-instance同一站点:根域名加上协议,还包含了该根域名下的所有子域名和不同的端口 A.fake.comwww.fake.comB.fake.com:789789这三个域名就是同一站点。站点隔离我们之前介绍浏览器渲染进程 -> 一般情况下,一个Tab页对应一个渲染进程。这里存在一个漏洞,页面中存在跨域的 iframe,该 iframe 就会有访问该渲染进程内存的权限,这就违背了,同源策略。 所以,在最新的chrome架构中,如果页面中存在跨域的 iframe,这些跨域的 iframe也会唤起一个属于自己的渲染进程。渲染流程总览虽然,该篇文章以 chrome浏览器来解释 浏览器的工作流程,但是我们还是需要对其他浏览器的渲染进程配置做一个汇总。 导航阶段浏览器的导航过程涵盖了从用户发起请求到提交文档给渲染进程的中间所有阶段。UI进程👉拼装URL当用户在地址栏中输入一个查询关键字时,地址栏会判断输入的关键字是搜索内容,还是请求的 URL。如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的 URL。如果判断输入内容符合 URL 规则,那么地址栏会根据规则,把内容加上协议,合成为完整的 URL。网络进程👉获取数据当最终的URL拼装好后,会由UI进程通过IPC通知浏览器进程,而浏览器进程会将消息传递给网络进程。(此时的浏览器进程充当一个消息中转站的角色) 当网络进程接受到来自UI进程需要网络连接的消息后。随之会初始化一个网络连接。该篇幅主要是讲解,浏览器如何渲染数据,所以浏览器如何和服务器建立连接等步骤,会一带而过。首先,网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。这请求前的第一步是要进行 DNS 解析,以获取请求域名的服务器 IP 地址。如果请求协议是 HTTPS,那么还需要建立 TLS 连接。接下来就是利用 IP 地址和服务器建立 TCP 连接。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。重定向在接收到服务器返回的响应头后,网络进程开始解析响应头,如果发现返回的状态码是 301(永久性的移动) 或者 302(临时性重定向),那么说明服务器需要浏览器重定向到其他 URL。这时网络进程会从响应头的 Location字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,一切又重头开始了。在导航过程中,如果服务器响应行的状态码包含了 301、302 一类的跳转信息,浏览器会跳转到新的地址继续导航;如果响应行是 200,那么表示浏览器可以继续处理该请求。根据Content-Type进行数据处理Content-Type: XX => XX是MIME类型的字符格式。MIME指示文档、文件或字节分类的性质和格式。Content-Type 告诉浏览器服务器返回的响应体数据是什么类型,然后浏览器会根据 Content-Type 的值来决定如何显示响应体的内容。不同 Content-Type 的后续处理流程也截然不同。如果 Content-Type 字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是Content-Type:text/html,那么浏览器则会继续进行导航流程。唤起渲染进程由于,浏览器和服务器之间数据的交互时间存在不确定性,在响应体回来以后,再唤起渲染进程是存在滞后的。所以在UI进程向网络进程发送拼装好的URL的时候,已经知道后续的导航的信息。此时UI进程会尝试随机唤起一个渲染进程。以备在响应体数据满足渲染要求,直接进行渲染操作。UI进程向网络进程发送URL 和 UI进程尝试唤起渲染进程是 同步进行的。更新Tab状态 -->导航阶段结束在响应数据和渲染进程准备就绪后,网络进程通过IPC向渲染进程传递提交响应体数据的消息。渲染进程和网络进程会建立数据通道。网络进程的数据就会源源不断的流向渲染进程。当数据都传送完毕后,渲染进程会向UI进程发送确认提交的消息。UI进程在接收到确认提交消息,更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。此时导航阶段正式结束,接下来进入页面渲染阶段。渲染阶段下面我们来重点讲述下,Chrome浏览器是如何将 HTML 和 CSS 拼装成浏览器可识别的信息。想必大家都见过这张图,该图显示的是 WebKit 内核的渲染页面主流程。之所以,我把这个拿出来,是因为 Chrome/Safari/Edge 的渲染引擎都是 基于 Webkit 改造而来的。所以,我们通过了解 Webkit 的渲染流程,就能通晓市面上大部分浏览器的运行流程。而上面所展示的图,是 2011 年所绘制的,现在都 1202 年了,有些流程和细节有变更和填充。但是,大体的流程和处理方式是一脉相承的。我们通过导航阶段,从服务器中获取到用于浏览器展示的数据信息-- HTML/CSS。但是,HTML和CSS都是文本信息,是无法被浏览器所识别和使用,所以就需要一个机制,让HTML等文本信息转换为浏览器能够识别的格式,而这个转换过程就是解析阶段。HTML和(CSS/JS)的解析方式是不一样的。编译器大多数编译程序(compiler)分为三个步骤:Parsing(分析阶段)/Transformation(转换)/Code Generation(代码生成或者说生成目标代码)Parsing将源代码(raw code)通过词法分析和语法分析转换为AST(抽象语法树)。Transformation接收Parsing生成的AST,并且按照compiler内定的规则进行代码的转换。Code Generation 接受被compiler转换过的代码,按照一定的规则将代码转换为最终想要输出的代码格式 通过以上三个步骤,大部分程序会被编译为目标代码。如果想了解更多关于编辑器是如何运行的,可以参考我原来写的编译程序(compiler)的简单分析。 在这里就不多啰嗦了。BNF常规的与上下文无关的语言,是可以通过 BNF 格式来描述。BNF: 一种 形式化符号 来 描述 给定语言 的 语法一种形式化的语法表示方法,用来描述语法的一种形式体系,是一种典型的元语言。它不仅能严格地表示语法规则,而且所描述的语法是与上下文无关的。它具有语法简单,表示明确,便于语法分析和编译的特点。BNF表示语法规则的方式为:①:非终结符用尖括号括起。②:每条规则的左部是一个非终结符,右部是由非终结符和终结符组成的一个符号串,中间一般以::=分开。③:具有相同左部的规则可以共用一个左部,各右部之间以直竖“|”隔开。例如,我们在解析 2 + 3 - 1 这个表达式时,词法规则,我们可以用:INTEGER: 0|[1-9][0-9]* PLUS: + MINUS: - 复制代码语法规则:expression ::= term operation term operation ::= PLUS | MINUS term ::= INTEGER | expression 复制代码生成的 AST 结构通过对AST进行个性化处理,最终生成指定机器和引擎能够识别的机器语言。在讲述BNF是啥的时候,我们提到了 与上下文无关这个概念。根据 维基百科的描述,我们可以简单描述下,在常规的语言中,如果是CFG的话,是可以描述为 ,而 是一个非终端符号或者标识,一般为终止符号或者说是Tokens(不可分割元素)。而一个语言,不需要考虑左值的上下文语境的时候,就可以使用BNF来表示。HTML 解析器既然,我们说HTML想要被浏览器识别,是需要被解析的,而由于HTML的语言特性或者独特的解析过程,HTML是不能使用常规 上下文无法的编译器进行转换的。理由如下:语言的宽容本质浏览器历来对一些常见的无效 HTML 用法采取包容态度解析过程需要不断地反复。源内容在解析过程中通常不会改变,但是在 HTML 中,脚本标记如果包含 document.write,就会添加额外的标记,这样解析过程实际上就更改了输入内容DOM而HTML解析器最终的目标就是为了,将HTML转换为浏览器能识别的数据结构 -- DOM。DOM(Document Object Model)的缩写,即文档对象模型。是针对XML并经过扩展用于HTML的应用程序编程接口(API)所以DOM本质上是一种接口(API),是专门操作网页内容的API标准DOM把整个页面映射为一个多层节点结构,HTML或XML页面中的每个组成部分都是某种类型的节点。借助DOM提供的API,开发人员可以删除、添加、替换或修改任何节点由于不能使用常规的解析技术,浏览器就创建了自定义的解析器来解析 HTML。此算法由两个阶段组成:标记化和树构建。标记算法标记化是词法分析过程,将输入内容解析成多个标记(tokens)。HTML 标记包括起始标记、结束标记、属性名称和属性值。这里有一个点需要注意:在 HTML2 -HTML4 中, 声明引用 DTD,因为 HTML 4.01 基于 SGML。DTD 规定了标记语言的规则(tokens),这样浏览器才能正确地呈现内容。HTML5 不基于 SGML,所以不需要引用 DTD。DOM树构建算法标记生成器识别标记,传递给树构造器,然后接受下一个字符以识别下一个标记;如此反复直到输入的结束。在创建解析器的同时,也会创建 Document 对象。在树构建阶段,以 Document 为根节点的 DOM 树也会不断进行修改,向其中添加各种元素。标记生成器发送的每个节点都会由树构建器进行处理。规范中定义了每个标记所对应的 DOM 元素在接收到相应的标记时创建。处理子资源网站中总是会嵌入图片、CSS、JS等非文本资源,而这些非文本信息需要再次从服务器或者缓存中获取。在DOM树构建过程中,如果遇到此类HTML标签,主线程将会依次请求对应的数据信息。而为了加快构建速度,预加载扫描器会和构建DOM树同步运行。 在标记化过程中,如果遇到类似或者标签,预加载扫描器会通知网络进程发起获取对应标签数据信息的异步请求。(此时主线程和网络请求是同步的)一切都归于完美,但是如果非文本标签是是</code>,就是另外一回事了。当标记算法输出的是<code><script></code>,此时HTML的解析过程就会停止,也就是说,主线程不会在继续解析tokens,转而去加载、解析、执行对应的JS代码。只有在JS代码执行完成以后,HTML的解析才会继续进行。</div><blockquote style="background-color: #F8F8F8;"><div data-lake-id="22e42b1e43bfe13651cde7d54b72d413"><strong>JS会阻塞DOM树的构建</strong> -->是由于JS代码中可能掺杂着类似于<code>document.write()</code>这种对于已经构建好的DOM树来说属于毁灭性打击的操作(直接将原来的树,全部抛弃)。</div></blockquote><div data-lake-id="c638e2db51953f90f388abc6b9356148"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_43e468df999d4ab69f20507a9323256c.png%22%2C%22originWidth%22%3A308%2C%22originHeight%22%3A400%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A308%2C%22height%22%3A400%7D"></span></div><div data-lake-id="b41b75833831ca2c83a68963ae54fe8a">由于<code><script></code>会阻塞主线程构建DOM树,所以如果<code><script></code>中不存在<code>document.write()</code>这种对已构建DOM树毁灭性打击的行为,我们可以通过对<code>script</code>设置<code>defer</code>/<code>async</code>属性来避免阻塞。</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%3Cscript%20async%20src%3D%5C%22A.js%5C%22%3E%3C%2Fscript%3E%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22f12bu%22%7D"></div><div data-lake-id="b8a9637f28b962dfe13cc772d98dec1e">有 <code>async</code>,加载和渲染后续文档元素的过程将和 A.js 的加载与执行<code>并行</code>进行(<strong>异步</strong>)。</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%3Cscript%20defer%20src%3D%5C%22B.js%5C%22%3E%3C%2Fscript%3E%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22hog1l%22%7D"></div><div data-lake-id="056651f715a8e0eae154302cb1068339">有 <code>defer</code>,加载后续文档元素的过程将和 B.js 的加载并行进行(<strong>异步</strong>),但是 B.js 的执行要在<strong>所有元素解析完成</strong>之后,<code>DOMContentLoaded</code> 事件触发<strong>之前</strong>完成。</div><div data-lake-id="062a57c9aa1f285397d502883656f555">从实用角度来说呢,首先把<strong>所有脚本</strong>都丢到 <code></body></code> 之前是最佳实践,因为对于<strong>旧浏览器</strong>来说这是<code>唯一</code>的优化选择,此法可保证非脚本的其他一切元素能够以最快的速度得到加载和解析。 接着,我们来看一张图咯: <span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_b0d785f5e9044feaaf431f811c1de058.png%22%2C%22originWidth%22%3A689%2C%22originHeight%22%3A112%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A689%2C%22height%22%3A112%7D"></span>蓝色线代表网络读取,红色线代表执行时间,这俩都是<strong>针对脚本</strong>的;绿色线代表 HTML 解析。</div><div data-lake-id="9e0d0ed31af2aa0850a696e36fcdcdba">此图告诉我们以下几个要点: <code>defer</code> 和 <code>async</code> 在网络读取(下载)这块儿是一样的,都是<strong>异步</strong>的(相较于 HTML 解析) 它俩的差别在于脚本下载完之后<strong>何时执行</strong>,显然 defer 是最接近我们对于应用脚本加载和执行的要求的</div><blockquote style="background-color: #F8F8F8;"><div data-lake-id="e07c903e9d7b5fba379970d9831dfed1">Note: <code>defer</code> 是按照加载顺序执行脚本的, <code>async</code> 是乱序执行的。</div></blockquote><div data-lake-id="16e83850128ca87b622521f1eae31c64">同时,我们可以通过<code><link preload/></code>对资源进行预加载处理。 具体如何使用,可以参考 <a href="https://link.juejin.cn/?target=https%3A%2F%2Fwww.cnblogs.com%2Fshenjp%2Fp%2F13029185.html" target="_blank" ref="nofollow noopener noreferrer">通过link的preload进行内容预加载</a>,描述的很详细。或者对性能优化感兴趣,可以参考外文 <a href="https://link.juejin.cn/?target=https%3A%2F%2Fweb.dev%2Ffast%2F%23prioritize-resources" target="_blank" ref="nofollow noopener noreferrer">Fast load times</a>(后续有计划做整理和翻译,敬请期待)</div><div data-lake-id="f91a2c22e0133d2d4c3c25190cba0a75">经过一顿操作,HTM解析器终于将HTML转化为浏览器能够识别的 <code>DOM</code> 结构。</div><div data-lake-id="53559360bb1c122da2bc575c867a9665">通过对百度首页渲染流程来简单看一下。 <span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_e0979c2585494093b6bc7967ba163616.png%22%2C%22originWidth%22%3A2202%2C%22originHeight%22%3A1556%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A2202%2C%22height%22%3A1556%7D"></span></div><blockquote style="background-color: #F8F8F8;"><div data-lake-id="6fff3cbb98a2fed7c21ad5fe0caffa20">最下方是显示有一个棕色线条 和用小圆圈标注的调用堆栈,表示DOM构建的过程。从图中可以看出,DOM是在HTM 解析阶段生成。</div></blockquote><div data-card-type="block" data-ready-card="hr"></div><h3 data-lake-id="5a63541ba7e8e5e55ac391fe48aba377" id="jJSSi"><br /></h3><h3 data-lake-id="59ba2e793e06187aaa71ab41bbc366a9" id="ZYYEe"><a ref="nofollow noopener noreferrer" href="https://link.juejin.cn/?target=" target="_blank">将CSS附加(attachment)到DOM节点==>生成Render Tree</a></h3><div data-lake-id="ce5c46e90728af16f60ac1e65e5cb3d8"><br /></div><div data-lake-id="2867783bf93718a28a320fc2d0ec65ca"><strong>解析样式</strong>和<strong>创建呈现器</strong>的过程称为“<strong><code>附加</code></strong>”。每个 DOM 节点都有一个“<code>attach</code>”方法。附加是<strong>同步</strong>进行的,将节点插入 DOM 树需要调用新的节点“attach”方法。</div><div data-lake-id="cd22e087269532ac553dee7f7de09f0c">处理 <code>html</code> 和 <code>body</code> 标记就会构建呈现树<strong>根节点</strong>。这个根节点呈现对象对应于 CSS 规范中所说的容器 <code>block</code>,这是最上层的 block,包含了其他所有 block。它的尺寸就是<strong>视口</strong>,即<strong>浏览器窗口显示区域的尺寸</strong>。 WebKit 称之为 <code>RenderView</code>。这就是文档所指向的呈现对象。呈现树的其余部分以 DOM 树节点插入的形式来构建。</div><h4 data-lake-id="e1046d917b95b126a521cc58221019bf" id="wznps"><br /></h4><h4 data-lake-id="4c1d706e89a9becdc19059bb389e9db0" id="UnonW"><a ref="nofollow noopener noreferrer" href="https://link.juejin.cn/?target=" target="_blank">CSS 解析器</a></h4><div data-lake-id="549321061ca3775e1bc7e3ec4c4a2cbe"><br /></div><div data-lake-id="ec5eddd9febb4d7fb23ae4eabf3e36ab">由于HTML解析和CSS解析都是在渲染进程中,并且渲染进程只存在一个主线程,也就意味着主线程在同一时间只能做一件事。--><strong>单线程特性</strong>。</div><div data-lake-id="1000be3f94d283307bc2967e6ed0a705">然后在DOM构建完成,并且将位置靠前的<code><script></code>也处理完,以后才会开始CSS的解析步骤。 <span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_08a9dea0162d44e980005d4111be8b11.png%22%2C%22originWidth%22%3A2276%2C%22originHeight%22%3A562%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A2276%2C%22height%22%3A562%7D"></span></div><div data-lake-id="41c1429d75e864408e45f752ed1a43cd">由于<code>CSS</code> 是<strong>上下文无关</strong>语言,所以解析CSS可以使用常规的编译器。而<a href="https://link.juejin.cn/?target=https%3A%2F%2Fwww.w3.org%2FTR%2FCSS%2F%23intro" target="_blank" ref="nofollow noopener noreferrer">W3C中CSS</a>定义了相关的<strong>词法和语法</strong>。</div><div data-lake-id="c7fdc45a6f134e3fe2ec1696d0278bec">解析器会将 <code>CSS</code> 文件解析成 <code>StyleSheet</code> 对象,且每个对象都包含 <strong>CSS 规则</strong>。CSS 规则对象则包含<strong>选择器</strong>和<strong>声明对象</strong>,以及其他与 CSS 语法对应的对象。</div><div data-lake-id="2f031c11a79de9e3d174517d6a78300d"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_fb8135e8b56e41b5bbadead9f0bb94d6.png%22%2C%22originWidth%22%3A500%2C%22originHeight%22%3A393%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A500%2C%22height%22%3A393%7D"></span></div><div data-card-type="block" data-ready-card="hr"></div><h4 data-lake-id="b6f3600754d75bbf194175984ea5362c" id="zAXb4"><br /></h4><h4 data-lake-id="21f6dde86e3acac6a6cabc496c5b06d6" id="NSqoU"><a ref="nofollow noopener noreferrer" href="https://link.juejin.cn/?target=" target="_blank">创建呈现器</a></h4><div data-lake-id="a42b9a15cd085072f4a7bfa1ec5e719a"><br /></div><div data-lake-id="b02e8533ca9dd0d57ca4c47d109db38f">这是由<code>可视化元素</code>按照其<strong>显示顺序</strong>而组成的树,也是文档的<strong>可视化表示</strong>。它的作用是按照正确的顺序绘制内容。</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22class%20RenderObject%7B%5Cn%20%20virtual%20void%20layout()%3B%5Cn%20%20virtual%20void%20paint(PaintInfo)%3B%5Cn%20%20virtual%20void%20rect%20repaintRect()%3B%5Cn%20%20Node*%20node%3B%20%20%2F%2Fthe%20DOM%20node%5Cn%20%20RenderStyle*%20style%3B%20%20%2F%2F%20the%20computed%20style%5Cn%20%20RenderLayer*%20containgLayer%3B%20%2F%2Fthe%20containing%20z-index%20layer%5Cn%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%228FguZ%22%7D"></div><div data-lake-id="2713a3c274ca763bfb22e37af3167530">每一个呈现器都代表了一个<strong>矩形的区域</strong>,通常对应于相关节点的 CSS 框,包含诸如宽度、高度和位置等几何信息。</div><div data-lake-id="e927239c942c09f5de6fdd0bb3ea9bd6">框的类型会受到与节点相关的“<code>display</code>”样式属性的影响。例如,针对 <code>display:block</code>的元素,它矩形区域默认独占一行,而 <code>display:inline</code>的元素,是具有包裹性。(其实,针对CSS中盒模型是一个很大的课题,这块可以参考张鑫旭大佬有关的讲解。同时,自己也会有一定的文档说明,最近在做总结和梳理,敬请期待!)</div><h5 data-lake-id="6cd78f50a4f373178cf95f2d7be769a4" id="50KYf"><br /></h5><h5 data-lake-id="899390006fc83f0f9ade1c5689335176" id="9DKh5"><a ref="nofollow noopener noreferrer" href="https://link.juejin.cn/?target=" target="_blank">属性标准化</a></h5><div data-lake-id="161f90dd2abba84a1b4caf0113178215"><br /></div><div data-lake-id="614c9e57344c269f8854658747d18f82">现在我们已经将 CSS 节点解析为 <code>RenderObject</code>,但是在写CSS的时候,我们会写诸如<code>font-size:2em</code>的条件,而这些<strong>em</strong>是相对值,不是一个定值,所以,我们就需要将如 <code>2em</code>、<code>blue</code>、<code>bold</code>,这些不容易被渲染引擎理解的值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是<strong>属性值标准化</strong>。</div><div data-lake-id="6242105f33f0025d7c9617db5b2618a4">如果存在如下的样式信息</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22body%20%7B%20font-size%3A%202em%20%7D%5Cnp%20%7Bcolor%3Ablue%3B%7D%5Cnspan%20%20%7Bdisplay%3A%20none%7D%5Cndiv%20%7Bfont-weight%3A%20bold%7D%5Cndiv%20%20p%20%7Bcolor%3Agreen%3B%7D%5Cndiv%20%7Bcolor%3Ared%3B%20%7D%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22yfpox%22%7D"></div><div data-lake-id="82c181773273d314ca480a60194825cb"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_752a9f488b4b4792a9d230b2cf496e83.png%22%2C%22originWidth%22%3A1142%2C%22originHeight%22%3A346%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A1142%2C%22height%22%3A346%7D"></span></div><h4 data-lake-id="10d0bf7b66fd41d7c7519e34404f1266" id="Uchg2"><br /></h4><h4 data-lake-id="bcda868580fbe6e9d1005f4e259d506f" id="gbxOI"><a ref="nofollow noopener noreferrer" href="https://link.juejin.cn/?target=" target="_blank">样式计算并保存到ComputedStyle</a></h4><div data-lake-id="8bb7d7c4a0f1ae2d47c61c67243a1f6c"><br /></div><div data-lake-id="f09448bb8e1109098dec692ee968581b">在样式标准化后,渲染引擎已经可以识别每个 <code>RenderObject</code>中所携带真正的数据信息了, 但是DOM 节点和 <code>RenderObject</code> 可能存在<strong>一对多</strong>的关系。所以,我们需要将这些信息进行融合,这样才可以将<strong>最后的样式</strong>信息作用到 DOM 节点上。</div><blockquote style="background-color: #F8F8F8;"><div data-lake-id="a682b21e10568a0b28aea1fdce185cfc">产生某个DOM 节点受多个样式影响的原因之一:CSS文件来源不定 </div><div data-lake-id="c9d20dc38cf43752799915a73fd67f30">CSS 样式<strong>来源</strong>主要有三种:</div><div data-lake-id="c3eb256b870b6c73276a89d243de4859">①: 通过 <code>link</code> 引用的<strong>外部</strong> CSS 文件</div><div data-lake-id="f083c5cba820dccb01f43190aa6e2e3f">②:<code><style></code>标记内的 CSS</div><div data-lake-id="05315ce841f502b7dae60eb76ad784f9">③:元素的 style 属性<strong>内嵌</strong>的 CSS</div><div data-lake-id="e2efb589e4d38b49e315b86311ade588"><br /></div><div data-lake-id="2aada59b254790fee94359ed94492820"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_ffc3b4d1ebfd4a4eb6ed321d4fada9ae.png%22%2C%22originWidth%22%3A1142%2C%22originHeight%22%3A585%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A1142%2C%22height%22%3A585%7D"></span></div></blockquote><div data-lake-id="ed79fbd1d84478a48a5280615f7e4e7e">某个样式属性的声明可能会出现在多个样式表中,也可能在同一个样式表中出现多次。这意味着应用规则的顺序极为重要。这称为“层叠”顺序。根据 CSS3 规范,层叠的顺序为(<strong>优先级从高到低,降序排序</strong>): <span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_82697a67391f4dd08850cd02972c0f9d.png%22%2C%22originWidth%22%3A1850%2C%22originHeight%22%3A902%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A1850%2C%22height%22%3A902%7D"></span>我们可以简化一下,就是</div><ol data-lake-id="a961543174479fa5e71eff6c2c149960"><li data-lake-id="55bd3f4ded145867c6f137598cc460ed" style="padding-left: 6px;">1. CSS3的<code>transition</code>--> 优先级最高</li><li data-lake-id="fc7999912c63832235dca2f2544e9fbb" style="padding-left: 6px;">2. 浏览器重要声明 --> user agent stylesheet 中存在<code>!important</code></li><li data-lake-id="d906f4046328e67ef51ece679a905f63" style="padding-left: 6px;">3. 用户重要声明 --> 用户,就是直接在浏览器写的带有<code>!important</code>的属性</li><li data-lake-id="e867167c51c9533ca705ae422c98c831" style="padding-left: 6px;">4. 作者重要声明 --> <code><link></code>/<code><style></code>/<code>style</code>属性中带有<code>!important</code>的属性</li><li data-lake-id="efef7ab7a2ca4203e9ec22aae5c63d30" style="padding-left: 6px;">5. 动画属性</li><li data-lake-id="85180b3e7e9df1bf0cdf30f3c0044259" style="padding-left: 6px;">6. 作者普通声明 --> <code><link></code>/<code><style></code>/<code>style</code>属性</li><li data-lake-id="a540473d21294ce2f8a37f836415c2b1" style="padding-left: 6px;">7. 用户普通声明 --> 用户设置的自定义样式</li><li data-lake-id="db044eed21b5b822a9d85521aacd2fd7" style="padding-left: 6px;">8. 浏览器声明 --> <code>user agent stylesheet</code>浏览器默认属性</li></ol><div data-lake-id="44aa3129c10769b8be2c4fa932dfa0f2">相关连接请参考 <a href="https://link.juejin.cn/?target=https%3A%2F%2Fwww.w3.org%2FTR%2F2021%2FWD-css-cascade-5-20210119%2F" target="_blank" ref="nofollow noopener noreferrer">www.w3.org</a> 和 <a href="https://link.juejin.cn/?target=https%3A%2F%2Fdrafts.csswg.org%2Fcss-cascade-4%2F%23important" target="_blank" ref="nofollow noopener noreferrer">css-cascade-4</a>。</div><div data-lake-id="ad41a20445821396e800ebea2cf2e60b">同时,如果不同样式都作用于同一 DOM节点,就需要有一个权重计算的规则。</div><div data-lake-id="58118af269ef8b9b551cd386e8f68ee6"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_3e57207b8d7b4c11a04c63ef80d2581e.png%22%2C%22originWidth%22%3A600%2C%22originHeight%22%3A764%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A600%2C%22height%22%3A764%7D"></span></div><div data-lake-id="25655e8c3177247522e43ac593dc8296">一图胜千言,有木有。</div><div data-lake-id="b340ba710318a32786a9813c860c0000">我们通过权重计算等操作,最后可以确定了针对指定DOM 节点所携带的在最终样式信息,而这些信息会被存到 <code>ComputedStyle</code> 结构中。 <span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_27fc57015a4b4618996e0bb4efe89032.png%22%2C%22originWidth%22%3A1612%2C%22originHeight%22%3A644%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A1612%2C%22height%22%3A644%7D"></span>如果在实际开发中,你用过<code>style = window.getComputedStyle(element); </code>该方法,返回了指定element所有的属性。而这个的方法返回的数据信息,其实就是通过一系列计算得到的<code> ComputedStyle</code>结构。</div><div data-lake-id="dec890c910d2452d6a42dbc444d09d59"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_9c5b263bcc6045568319cdb0f70fcdf5.png%22%2C%22originWidth%22%3A731%2C%22originHeight%22%3A396%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A731%2C%22height%22%3A396%7D"></span>最后,我们得到了,一棵通过向 DOM树 <strong>附加</strong>样式信息的 <code>Render Tree</code>。</div><div data-card-type="block" data-ready-card="hr"></div><h2 data-lake-id="af8fc6fa756fbe8929a7fd15aac811dc" id="7Sqdh"><br /></h2><h2 data-lake-id="98ad7f3e4bf3af919aa930fc008c6c9d" id="577Ep"><a ref="nofollow noopener noreferrer" href="https://link.juejin.cn/?target=" target="_blank">布局(Layout)</a></h2><div data-lake-id="fa46378597b7d5c980a8b78c98742842"><br /></div><div data-lake-id="375ba4b37017fa376ead175f404fa8a0">通过HTML 解析和 CSS 解析,已经将HTML和CSS信息融合到一起,并且知道了每个节点各自的<strong>外观样式</strong>信息。但是光有外观样式信息还是不能够将节点排布到他们真正需要渲染到页面中的位置。还需要该元素对应的位置和大小信息。</div><div data-lake-id="793a21b2ce447a5bb409a35f75894e8b">呈现器在创建完成并添加到呈现树时,并<strong>不包含位置和大小信息</strong>。计算这些值的过程称为<strong>布局</strong>或<strong>重排</strong>。</div><div data-lake-id="3640566f7a6b2de265991315f53f6c08">HTML 采用<strong>基于流</strong>的布局模型,处于流中<strong>靠后位置</strong>元素通常不会影响靠前位置元素的几何特征,因此布局可以按<code>从左至右、从上至下</code>的顺序遍历文档。</div><blockquote style="background-color: #F8F8F8;"><div data-lake-id="277173f9a68105a54cd211ed39e4c723">坐标系是相对于根框架而建立的,使用的是<code>上坐标</code>和<code>左坐标</code>。</div></blockquote><div data-lake-id="bc02bc294ef14f4033ceb084431504f0">布局是一个<strong>递归</strong>的过程。它从<strong>根呈现器</strong>(对应于 HTML 文档的 元素)开始,然后递归遍历部分或所有的框架层次结构,为每一个需要计算的呈现器计算几何信息。</div><div data-lake-id="4c8b3c23f9bc40ca8722848b77e47d27">根呈现器的位置<strong>左边</strong>是 <code>0,0,</code>其尺寸为视口(也就是<strong>浏览器窗口的可见区域</strong>)。 所有的呈现器都有一个“<code>layout</code>”或者“<code>reflow</code>”方法,每一个呈现器都会调用其需要进行布局的子代的 layout 方法。</div><div data-lake-id="f35753b0c1ab9b46c0e0fa34de876a7c">布局是一个寻找元素<strong>几何形状</strong>的过程。主线程遍历DOM和计算样式并创建布局树,布局树包<strong>含诸如x y坐标和边框大小等信息</strong>。呈现器是和 DOM 元素相对应的,但<strong>并非一一对应</strong>。非可视化的 DOM 元素不会插入布局树中,例如“<code>head</code>”元素。如果元素的 <code>display</code> 属性值为“<code>none</code>”,那么也不会显示在呈现树中(但是 visibility 属性值为“<code>hidden</code>”的元素仍会显示)。</div><blockquote style="background-color: #F8F8F8;"><div data-lake-id="30326da32c49cd7816b5df75668e733c">有一些呈现对象对应于 DOM 节点,但在树中所在的位置与 DOM 节点不同。<code>浮动定位</code>和<code>绝对定位</code>的元素就是这样,它们处于正常的流程之外,放置在树中的其他地方,并映射到真正的框架,而放在原位的是<strong>占位框架</strong>。</div></blockquote><div data-lake-id="aecb996211306617fb3de7197e4798b7"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_5bb8a55984c147bb8366b52d2aef30f1.png%22%2C%22originWidth%22%3A1610%2C%22originHeight%22%3A662%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A1610%2C%22height%22%3A662%7D"></span>主线程遍历Render Tree 然后生成Layout Tree (布局树)</div><div data-card-type="block" data-ready-card="hr"></div><h2 data-lake-id="ff84b26a74cf3c1883c162fae1f7b37d" id="7ExlA"><br /></h2><h2 data-lake-id="e2e6f24d1391cc58749296409822b24b" id="zLXkJ"><a ref="nofollow noopener noreferrer" href="https://link.juejin.cn/?target=" target="_blank">绘制(Paint)-生成元素绘制顺序</a></h2><div data-lake-id="9825f1d51d7ffb3f2ad5d3dd61ece0e7"><br /></div><div data-lake-id="aa9372c1bf28d5ccf31d8ca984a09c3c">通过,布局处理,已经知晓所以元素的大小,位置。但是,还是不能进行<strong>按部就班</strong>的进行页面渲染,虽然HTML 采用<strong>基于流</strong>(<code>从左到右,从上到下</code>)的布局模型进行布局,但是通过样式,可以脱离了流的默认流向和渲染顺序。</div><div data-lake-id="a37628c31518d1d68084859ff3159339">例如我们可以通过<code>z-index</code>将在Z-轴方向搞点事情,这里就涉及到一个新的概念--层叠上下文 (这玩意也是一个很大的课题,如果有兴趣了解的话,还是可以参考张鑫旭大佬写的<a href="https://link.juejin.cn/?target=https%3A%2F%2Fwww.zhangxinxu.com%2Fwordpress%2F2016%2F01%2Funderstand-css-stacking-context-order-z-index%2F" target="_blank" ref="nofollow noopener noreferrer">深入理解CSS中的层叠上下文和层叠顺序</a> ) <span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_591309bab74b4492829ddd49d0d79343.png%22%2C%22originWidth%22%3A599%2C%22originHeight%22%3A375%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A599%2C%22height%22%3A375%7D"></span></div><div data-lake-id="d8ed85ded9da7068f4f1f4f181e141d0">直接上图,具体实现和讲解,就先不讨论了。</div><div data-lake-id="0216f1db0de4bebb09dce8d55fb6f533">所以渲染器就从布局树的根节点进行遍历,按照<strong>各个维度</strong>进行最后的渲染顺序的确认,并生成元素的绘制顺序(paint record)。</div><div data-lake-id="14bfb1b54a783634eafc7dbb962db66a"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_3946b499c8184680b4bdde521272a21f.png%22%2C%22originWidth%22%3A1612%2C%22originHeight%22%3A658%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A1612%2C%22height%22%3A658%7D"></span></div><blockquote style="background-color: #F8F8F8;"><div data-lake-id="7aafe59a0f78e1ed344d89e263c491dc">绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些<strong>待绘制列表</strong>。</div></blockquote><div data-card-type="block" data-ready-card="hr"></div><h2 data-lake-id="47c26f52f073542eae567200f8b4cf7e" id="qW2tS"><br /></h2><h2 data-lake-id="046e276eece3b07aa4b728d516106c7d" id="HdN8r"><a ref="nofollow noopener noreferrer" href="https://link.juejin.cn/?spm=a2c6h.13046898.0.0.314e6ffaMwOelL&target=" target="_blank">页面合成(Compositing)-->先分层,栅格化->页面合成</a></h2><div data-lake-id="724c1cc646edc87586cbff7672374cdc"><br /></div><div data-lake-id="c310996cd5deaf56b74b13dd2b7acdb9">页面合成是一种技术,将页面的各个部分分离成层,分别<strong>栅格化</strong>它们,并在称为<code>复合线程</code>的单独线程中复合为页面。如果发生滚动,因为图层已经栅格化了,它所要做的就是合成一个<strong>新帧</strong>。动画也可以通过移动图层和合成新帧来实现。</div><h3 data-lake-id="09e060eeb5c0bf2b1906316758e34020" id="8WlpP"><br /></h3><h3 data-lake-id="a42e4cce1b056538ea8d7865dda2a9c8" id="a1V3t"><a ref="nofollow noopener noreferrer" href="https://link.juejin.cn/?target=" target="_blank">分层</a></h3><div data-lake-id="747c98faa5837e4baec6b79865aeb7db"><br /></div><div data-lake-id="2d152d7fc9714e2e71bd37c3c789f972">现在我们已经知道了,元素之间的<strong>绘制顺序</strong>,此时如果一股脑的从根节点开始渲染,将会是一项很大的工程,所以渲染引擎为<strong>特定的节点</strong>生成专用的图层,并生成一棵对应的图层树(<code>LayerTree</code>)--> <strong>分而治之</strong></div><blockquote style="background-color: #F8F8F8;"><div data-lake-id="c9924e9e4fd95f4dcd5993ae51aa3183">浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面</div><div data-lake-id="1434b1969b42772f928d73d51d402e45"><strong>分层的依据就是根据<code>层叠上下文</code>将页面分成不同的图层。</strong></div></blockquote><div data-lake-id="9c2de38f6e86ba8de59ffc6eb0a6ad71"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_803db348c4144caaa0c561eb163bfd25.png%22%2C%22originWidth%22%3A1142%2C%22originHeight%22%3A674%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A1142%2C%22height%22%3A674%7D"></span></div><div data-lake-id="2cd11fa23df81047b3149a3673759a58"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_80c3fdc827f0471fb998dc8383bcc010.png%22%2C%22originWidth%22%3A1600%2C%22originHeight%22%3A660%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A1600%2C%22height%22%3A660%7D"></span></div><div data-lake-id="1d3384583437a5eb76678ea6b79005b9">完成了图层树的分割,主线程就开始遍历图层,并且生成一系列<strong>渲染记录</strong> -> 用于指示渲染引擎对该图层的渲染顺序。</div><h3 data-lake-id="70faa851e087dde74dc611728bd77f5d" id="7Ymrc"><br /></h3><h3 data-lake-id="99a79a545006e6e426fd6fd810784453" id="hRO5U"><a ref="nofollow noopener noreferrer" href="https://link.juejin.cn/?target=" target="_blank">栅格化(raster)操作</a></h3><div data-lake-id="a29046ec759ce8e466dee25731c4a6e0"><br /></div><blockquote style="background-color: #F8F8F8;"><div data-lake-id="9f40ed6933283dd7b15d708c9ae72d76">栅格化:将待绘制列表转换为屏幕中的像素</div></blockquote><div data-lake-id="8f97510a80bec4ad5f2532b077687841">绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的<code>合成线程</code>来完成的。</div><div data-lake-id="e8a38dd27a5dd22ce4710240ca6dd84e">通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(<code>viewport</code>)</div><div data-lake-id="7846c785620ef8db26621aab310590a0">在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。</div><div data-lake-id="3772b8ec20fad3d6e0679db9b19da0e9">基于这个原因,合成线程会将图层划分为<strong>图块(<code>tile</code>)</strong>,这些图块的大小通常是 <strong>256x256</strong> 或者 <strong>512x512</strong></div><blockquote style="background-color: #F8F8F8;"><div data-lake-id="c6078f2365ac7e8a6eb9c1db3bda7095">合成线程会按照<strong>视口附近</strong>的图块来优先生成位图,实际生成位图的操作是由<code>栅格化</code>来执行的。所谓栅格化,是指将<code>图块转换为位图</code>。而<strong>图块是栅格化执行的最小单位</strong>。渲染进程维护了一个栅格化的<code>线程池</code>,所有的图块栅格化都是在线程池内执行的</div></blockquote><div data-lake-id="e1baca41453b56bf364aa3027d5bb946"><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fzd7oztix3a4mm_373cc60389fe48e7a92251e2757c1fd8.png%22%2C%22originWidth%22%3A1142%2C%22originHeight%22%3A677%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A1142%2C%22height%22%3A677%7D"></span></div><div data-lake-id="7305a469e97c9fc4e8d410f2f23bd090">在处理完一帧的数据以后,就会向合成线程就会通过IPC将处理好的数据,返回给浏览器进程用于显示页面。而<strong>不占用渲染进程的主进程</strong>。</div><div data-lake-id="431225cae59263df7ab455aeb41cc364">如此往复,直到合成线程中栅格化线程池中数据都被消费掉,页面也就渲染好了。</div><div data-card-type="block" data-ready-card="hr"></div><div data-lake-id="fae2f917cca536d8090d82a63f8f38ef">备注:该篇文章参考了很多资料,算是一个大杂烩。如果大家有兴趣,想看原文,可以直接参考。</div><ul data-lake-id="812697065d989a566f0e773d4fc386cb"><li data-lake-id="ec51bb2de55b87df5bb89596ca46369f"><a href="https://link.juejin.cn/?target=https%3A%2F%2Fwww.html5rocks.com%2Fen%2Ftutorials%2Finternals%2Fhowbrowserswork%2F" target="_blank" ref="nofollow noopener noreferrer">How Browsers Work: Behind the scenes of modern web browsers</a></li><li data-lake-id="488fcc401d72a46d7da41a3a97125624"><a href="https://link.juejin.cn/?target=https%3A%2F%2Fdevelopers.google.com%2Fweb%2Fupdates%2F2018%2F09%2Finside-browser-part1" target="_blank" ref="nofollow noopener noreferrer">Inside look at modern web browser </a></li><li data-lake-id="9a0eea71a50cce5bf275656fb01455aa"><a href="https://link.juejin.cn/?target=https%3A%2F%2Fwww.w3.org%2FTR%2F2021%2FWD-css-cascade-5-20210119%2F" target="_blank" ref="nofollow noopener noreferrer">w3c</a></li><li data-lake-id="9930a2e990b6d6928e95d54dbe0ef84d">极客时间的课程</li></ul>
如果你觉得可以,请多点赞,鼓励我写出更精彩的文章🙏。如果你感觉有问题,也欢迎在评论区评论,三人行,必有我师焉概要组件封装RenderProps增强版Web Component配置化(LCD)逻辑复用中间件(MiddleWare)Redux Thunk监听historystateComponment这篇文章主要目的是和大家探讨一下,针对框架使用者(React/VUE),如何合理的进行组件封装和逻辑复用。这里有一个前提条件--在平时以业务驱动的开发流程中,我们如何思考或者如何处理才能做到又快又好。 同时,由于我的开发技能包为React,所以更多的我会从React的角度来思考和解答问题。时间不早了,该干点正事了,咱们书归正转。 (郭德纲语音包) 👈 点击触发组件封装作为一名合格的Reacter来说,组件封装这个技能点就和相声演员口中的说学逗唱一样,是需要刻在骨子里的基本功。而React官方为我们提供了一些常规的操作方式,其中不乏HOC,RenderProps。如果对这些方式的如何使用还无法做到信手拈来。那可以参考我早些时候,写的React组件封装思路拓展 (可能当时文笔和技术不是很好,如有不对的地方,轻喷,我怕疼。)当然,现在技术也很渣{手动狗头}但是对于封装这个事来讲,There are a thousand Hamlets in a thousand people's eyes。 只所以造成这种,群魔乱舞,鬼魅丛生的现状,我认为最关键的一点,就是React它所选用的构建UI的JSX,本身就是一种JS的扩展。而JS最大的特点就是万物皆可对象,而对象的灵活性可以说是 无孔不入,防不胜防。RenderProps增强版在React官网中给出了一种关于组件封装的一种方案。组件属性中有一个属性,而该属性是一个函数,函数返回了一个组件。<RenderProps renderRegion={(value)=><CommonConponenet value={value}/>} 复制代码这种情况是可以满足很大部分的组件封装。但是鄙人认为,这样处理是可以将一部分可以抽离的功能和主UI进行分割。实现了一种叫做 面向切片编程的编程思路。用RenderProps进行可复用组件的抽离,固然好,但是,但是,但是,这里是一个转折,也不知道大家有没有维护过老代码或者是存在一个需求。一个主UI中,存在很多零散的操作区域,而这些零散的操作区域又可以进行封装。我们来做一个简单的描述。假如现在我们接手了如上图的需求,但是由于其每个操作区域(Area),都可能存在很多的情况,而此时为了践行 职责单一的设计模式。当然,也不是非要按照书本上的观点或者条条框框去做事,而我描述这个,只是为了师出有名(朱棣夺取皇位还想出了,清君侧呢。最近在拜读<<明朝那些事>>,敲击好看)。我们尝试将其每个部位都封装了组件。其实这种处理方式没有啥不好的地方,如果硬要是鸡蛋里挑骨头的话,就是在实例化父 组件的时候,看不出这些被抽离的组件是显示的什么逻辑。如果页面效果简单或者是需要抽离的部位不是很多,还好。但是,如果存在某些极端情况,一个组件内需要被抽离的东西过于零散,并且没法进行合并处理,这就在后期维护产生了隐患。同时,其实有些页面的功能是和页面结构强耦合的。有些同学有时候,陷入了一种 为了封装组件而封装组件的陷阱。将本来隶属于同一页面单独使用的逻辑,抽离成小组件,然后用各种props或者其他的状态管理去维护回调和参数。在利用RenderProps进行组件封装的时候,1. 被抽离组件过于零散,看不出UI是如何构建和组装的。2. 陷入为了将UI分割,而将逻辑分割的错误泥沼所以,为了解决这种让人感觉无关痛痒的问题,在RenderProps这种方案的基础上做了一个变种或者升级。我们再次将上述情况,精细化描述,假如RightBottomArea是一组button的集合,并且在项目开发中,多个页面存在与Parent组件相似的功能,但是展示的数量和操作都存在差异。我们在设计Parent的时候,就将UI分块处理,让它能够有可配置的能力。在常规开发中,我们遇到上面的需求,第一想法,就是构建一个专门维护Button的组件(就像第一个例子中LeftTopArea等),在调用Parent的时候通过传递不同的参数 pageSoucre = pageA或者pageB。来区分不同页面,并且在组件内部进行三元或者多元判断。下面,我们来简单说一下,我对这个问题是如何处理。我们先来看一下组件是如何调用的。 看到这里,细心的朋友,脑子可能存在一些疑问。1 .button的click事件如何绑定,并且该回调函数还是一个异步处理2 . 如果存在每个button的样式需要定制化需要如何处理客官别急,菜已经在锅里了,马上来。 这里简单的解释一下,这里面用的API的思路。React.Children.map 用于遍历组件的children数据和数组map是一个用法.React.cloneElement 用于复制出一个新组件,该组件的type,props都和源组件一样。并且,还可以在复制的时候,对新组件的属性(props)进行个性化的处理。总结:不知道,大家在开发过程中,用没用过这些API,并且在这些API现有功能基础上,做对自己开发有力的应用。其实,其中用这个点的来源,主要来源于React.createElement()。它是JSX的背后的男人,并且它返回的是一个对象。这就和JS一切皆对象的宗旨很契合。既然是对象了,只要能拿到对象的实例,是不是我们就可以为所欲为了。Web Component该技术是一种全新的组件封装的方式或者思路。他利用浏览器API来赋予你封装组件的能力。Web Components is a suite of different technologies allowing you to create reusable custom elements — with their functionality encapsulated away from the rest of your code — and utilize them in your web apps.上面描述是摘录至 MDN_Web_Components。从如上的描述中,我们可以得出几点1 构建可复用自定义元素2 将构建的元素和现有代码进行分离,使用了 Shadow DOM因为,这个组件封装的技术方案,我在实际项目中,只简单运用了下,但是感觉该技术方案是以后的趋势(无论你使用React还是Vue的框架),所以有必要,舔着脸来给大家讲述一下。(如果有不对的地方,欢迎指出)。我们在项目开发中,存在某种展示效果,就是文案超出指定宽度,需要显示... 或者某个文案需要多行展示。常规的处理,我们是在一个div或者span中定义某个class然后我们用JSX来简单展示一下如何展示使用。 虽然我们通过CSS自定义属性减低了代码量,但是像此类的功能在一个系统里,是很常见的。每次使用都写对应的样式(当然,也可以将此类样式维护到全局),然后再指定的组件或者元素中引用。无形中增加了维护成本。当然,有些同学,可能会想到,将此类属于 辅助或者展示功能并且与业务耦合度不是很高的片段,构建成组件。这种思路是对的,但是每次在需要使用此类功能片段,就需要将组件 import到目标文件。如果类似的组件很多,在组件的头部,很有很多 相关组件的import。这样存在潜在的问题,1. 如果功能片段组件路径变更,那引入的地方也需要跟着变动(维护成本偏大 )2. 有些功能片段代码量很大,并且没有做按需加载,那页面首次加载会很慢(加载速度 )3. 在调用功能组件的头部,代码臃肿。(不美观 )4. 抽离的组件,可能和原有组件样式冲突 (存在冲突)5. 无法跨框架(Vue/React)(框架局限)6. ......那如果我们按照 Web_Components来重构一下刚才的代码。 通过extends HTMLElement然后继承相关属性。我们可以和引用组件一样来讲自定义元素实例化。 这不失为一种好方法,但是我们是不是可以换一种思路,在我们项目的稍微顶级目录或者组件中,将元素实例化,这样就是一次实例化,处处调用。省去了很多功夫。虽然,在实际项目中,没有更多的尝试,但是现在无论Vue还是React都尝试向Web Components的方向靠拢。配置化(LCD)低代码开发(Low-Code Development,LCD),是一种很早被提出(2011)的开发模式,开发者主要通过图形化用户界面和配置来创建应用软件,而不是像传统模式那样主要依靠手写代码。其中就有一种基于编写 JSON的低代码开发。而我们所讲的这种方式,严格意义上算不上LCD,但是我们可以借助这个思想来进行组件的封装。做过ToB业务的同学可以知道,我们在进行表单查询的时候,总是会出现奇奇怪怪的筛选条件,而这些筛选项有时候就是一个简单的Input或者是Select,稍微复杂点,就会在筛选的时候,加入一些自己封装的自定义组件。我们按Antd的组件来简单规划下如上的筛选部分。 我们通过常规的方式来处理这段逻辑,在没有完全写完的情况下,对应的事件回调就存在3个以上(基本上每个组件一个回调函数),然后一些类似的功能也是重复率很高。这样一味的去堆砌代码,那么就是一个查询相关的代码就会变的又臭又长。不管是对自己还是以后接手维护这段代码的人来讲,都是一段煎熬,那么如何才能更加优雅或者更加让代码看起来简洁呢,我这里模仿LCD的实现方式。简单描述一下。 虽然大家看到用了LCD的这种方式来处理代码,表面上代码量增加了,但是这其实有一个前提条件的。就如我们上图说描述的,一个筛选框存在8个以上的条件,你就需要写对应的回调和配置信息,而这些配置信息其实都是类似的。并且代码还不好追踪。我们来简单的换算一下,我们用最原始的方式来构建筛选项,我们构建了5个组件,代码有50多行,这还是在有些组件没有配置必要的参数和回调的情况下,如果存在10个以上或者更多,按照现在的换算,保守估计,实现一个10个以上的筛选项并且存在多个自定义的组件,代码量至少300-400行起步。而我们利用LCD的思路来处理代码,这段代码最多200行搞定。代码量上直接节省 50%,这还是保守计算。大家可以看看平时写此类代码,可以对比一下代码量。就会一目了然了。这里再给大家唠叨一下,关于我是如何对这段代码进行抽离和封装的。1 利用了React本事支持的事件代理,将处理事件挂载到父级元素。(React其实也是这么做的)2 针对自定义组件我们通过配置信息中的instance来指定,这是利用React中的JSX本身就是JS语法的扩展 - 一切皆对象3 利用了自定义属性,进行数据的传递逻辑复用其实组件封装就属于逻辑复用的一种,但是我还是想单独想聊一下除了组件封装以外的数据/逻辑处理,因为有些思路和方法是从另外的一个角度来描述和思考的。中间件(MiddleWare)大家接触过Node的experss的开发,里面充斥着各种中间件。我认为中间件的作用就是面前切片编程的一种前端实现。而我们此次聊的不是聊Node,我们还是将目光关注于前端页面的处理。如果大家用React同时由于项目比较臃肿,单纯的利用React提供的context,state来维护页面直接的数据,就会变的杂乱无章。所以Redux就横空出世了。我们不讲如何配置和使用Redux,我相信市面上有很多关于这些的教材。我们要讨论的是,如何利用Redux来进行逻辑抽离和复用。案例一 改造Redux-thunk在项目开发中,我们希望在触发接口发送的时候,有一个前端Loading的效果处理,然后接口请求成功或者失败的时候,将Loading进行移出。(我们利用axios也是可以拦截的)这里还需要唠叨一下Redux-thunk,其他代码就几行,作为Redux的异步处理的中间件,就是为了处理异步action。实现控制反转的功能。既然是处理异步,我们是不是可以在这里搞点事情做。如果异步action是一个promise,我们就将处理一些loading的逻辑。这里有一个前提条件,就是在发起接口处理,是需要利用dispatch来进行包装的。1 我们在异步操作触发的时候,将store中的loading置为ture2 在接口有结果(失败/成功)以后, 再将store中的loading置为false3 同时配合一个全局的Loading组件来监听store中的loading来显示对应的操作案例二 监听history在实际业务中,有些操作是需要判断指定的URL变化或者出现就需要做一些额外的操作。比方说,针对某些特定的页面,需要为这页面的资源进行加锁处理。而针对SPA的场景中,基本上都是一个URL对应一个资源。所以,我们可以通过监听History的变化来做指定的操作。 我们可以通过,在某个位置,监听store中的objectId和isTriggerLock来做一些和后台交互的处理。 这样做的好处是1 通过中间件的方式,将逻辑抽离到框架层面,和业务代码耦合度不那么高2 如果以后某个页面或者某个模块需要此类的功能,迁移效果好(新增配置项就可以)其实这还有一个很重要的点,这块是需要接收一个history, 而这个history是通过 history这个库createBrowserHistory来实例化的。所以才可以通过监听 @@router/LOCATION_CHANGE进行处理。 (这是我通过读源码偶然发现的一个小玩意)stateComponmentstateComponment 这个词语是我自己创造的,他的含义或者用法-就是将组件存储到组件的state变量中。通过setState来切换组件的指向,从而实现页面的变换。我们简单描述下,只提供简单的思路。假如我们页面中存在很多操作,而这些操作都对应着一个modal。我们在平时所写代码中,基本上都是一个button -> visible -> modal其实在我看来,visible变量就是一个累赘,同时如果存在多个modal的情况下,也是需要一些额外的参数去维护。那么我们化繁为简,通过将组件存储于state中,是不是会变的更加方便。这里举了一个很常见的栗子,我们完全可以把Modal置换为其他自定义组件或者元素。其实,我想通过这个例子来说明下,针对于React项目开发中,组件其实就是一个对象,而JS中一切皆对象,并且它的灵活性是很高的。我们不必拘泥于常规的组件使用方式。我们就按照写JS变量的方式来处理组件就OK其实,关于逻辑复用这一块,有一点是绕不开的,那就是自定义Hooks。但是如果简单讲一下,总感觉讲不透彻的感觉,所以,我打算专门写一篇,关于自定义Hook的见解和看法。
如果你觉得可以,请多点赞,鼓励我写出更精彩的文章🙏。如果你感觉有问题,也欢迎在评论区评论,三人行,必有我师焉TL;DR前言常规组件化思路Hooks组件非常规组件(有奇特的效果,重点是这个)前言在我的一些文章中,不管是自己写的还是翻译国外优秀开发人员博客的。其中一个主线就是,了解了JS基础,来进行前端模块化实践。尤其现在前端比较流行的框架React、Vue都是提倡进行对页面利用组件进行拆分。这其实就是MVVM的设计思路。而我们今天以React项目开发,来聊聊如何在实际项目中实现组件化,或者如何进行高逼格的逻辑服用。以下所讲内容,都是本人平时开发中用到的,并且都实践过的(如果大家有更好的想法,也可以留言讨论,毕竟每个人对一个事件的看法或多或少有一些认知的差异)。我信奉一句话,实践出真知,没有实践就没有发言权常规组件化思路这里起的标题是常规,是因为这个篇幅中所讲的内容也是React官方毕竟推崇的常规方案(非常规方案,例如Hooks由于API比较新颖对于一些开发中,比较陌生,同时受公司React版本的影响,只是停留在理论阶段。我将Hooks会单独拎出来,讲一些小🌰)如果大家经常翻阅React官网,在ADVANCED GUIDES中明确指出了,两个实现逻辑服用的方式:HOC和Render Props。所以我们来简单介绍一下。HOC来看一下官方的解释:A higher-order component (HOC) is an advanced technique in React for reusing component logic.其实如果对JS高级函数有过了解的童鞋,看到这个其实不会感到很陌生。而HOC就是模仿高级函数,来对一些比较共用的逻辑,进行提炼。从而达到代码复用的效果。继续给大家深挖一下,高阶函数可以接收函数做为参数,同时将函数返回。function hof(f){ let name="北宸"; return f(name); } function f(name){ console.log(`${name} 是一个萌萌哒的汉子!`) } 复制代码看到这点是不是有点闭包的身影。也就是说,被包裹的函数拥有hof中所定义变量的访问权限。如果大家想对闭包、作用域有一个比较深刻的了解可以参考1. JS闭包(Closures)了解一下2. 带你走进JS作用域(Scope)的世界这题有点跑偏了。我们进入正题。const EnhancedComponent = higherOrderComponent(WrappedComponent); 复制代码这是对HOC的一个简单公式的概括。通过刚才讲解hof其实对这个就轻车熟路了吧。Talk is cheap,show your the codefunction EnhancedComponent(WrappedComponent){ //其实,这里也可以定义一些比较共用的参数和逻辑 return class extends React.Component{ // state={ //定义一些公共属性 name:'北宸' } componentDidMount() { //做一些数据初始化处理 } componentWillUnmount() { //针对一些注册事件例如轮询事件的解绑 } handleChange() { //自定义事件处理 } render() { //这里最好做一下结构处理 const {name} = this.state; return <WrappedComponent parentData={name} //将一些共用的数据传入 {...this.props} //简单的传入传出 />; } } } 复制代码这就是一个简单的HOC例子。 如果大家想对HOC有一个更深的了解可以参考1. React-HOC了解一下(这是我早期写的,可能有点Low)Render Props这也是一个常规逻辑复用的方式。先来一个总结,其实就是在组件属性中有一个属性,而该属性是一个函数,函数返回了一个组件。<RenderProps renderRegion={(value)=><CommonConponenet value={value}/>} 复制代码稍微解释一下RenderProps组件包含中一些共有逻辑和数据,而这些逻辑和数据,恰恰是CommonConponenet需要的。Talk is cheap,show your the codeclass RenderProps extends React.Component{ state={ name:'北宸' } //一堆生命周期方法 //一堆自定义方法 render( const {name} = this.state; return ( <section> <> //RenderProps自己的页面结构 </> //如果传入的数据过多,这里可以使用一个对象把数据进行包装 {this.props.renderRegion(name)} </section> ) ) } 复制代码想必,大家在平时开发中,或多或少都用到这些比较常规的方式。上面只是简单的介绍了一下。其实本文的重点是下面的一些非常规操作。Hooks组件想必大家在翻阅一些技术文档,有很多,关于Hooks用法的介绍。本文就不在啰嗦了,而今天给大家准备说一些非常规操作。如果实在想了解可以参考我翻译的一篇关于Hooks的简单应用。1. 后生,React-Hooks了解一下首先,有一点需要明确,hooks的推出,是针对函数组件。是让函数组件,也能享受state/生命周期带来的愉悦感。给大家一个应用场景,就是有一个组件,有一个定时功能,需要一个定时器,暂且定位15min内定时器走完,并在定时器完成之后进行额外的操作。通过上面的需求,我们来分析一波。1. 有一个定时功能,那势必就是在组件初始化时,要触发这个定时器,在触发定时器2 之后,定时器会额外的开出一个线程进行数据处理。2. 每次时间变化,都需要显示到页面中3. 我们需要监听定时器是否到达要求,(15min之后,需要额外操作)4. 触发额外操作5. 如果在15min之内,把组件销毁,还需要将定时器销毁Talk is cheap,show your the codeimport React,{useState,useEffect} from 'react'; function Test(props){ //从父组件来的数据 const {name}=props; const [msg, setMsg] = useState("二维码有效时间:15分00秒"); let timer, maxtime = 15 * 60; //1 初始化一个定时器,(在组件初始化时), useEffect(()=>{ timer = setInterval(() => { if (maxtime >= 0) { let minutes = Math.floor(maxtime / 60); let seconds = Math.floor(maxtime % 60); let msg = "二维码有效时间:" + minutes + "分" + seconds + "秒"; --maxtime; //2. 这里定义一个state,变量,用于页面显示 setMsg(msg) } else { clearInterval(timer); //如果在正常情况下时间走完,进行额外操作 //3. 这里可以是更新页面,也可以进行事件回调 } }, 1000) return ()=>{ clearInterval(timer); //在组件销毁时,将定时器销毁 } },[])//这里为什么需要第二个参数?可以参考我刚才给的链接,寻找原因 function stopTimer(){ clearInterval(timer); } return( <section> <Button onClick={()=>{this.stopTimer()}}>停止计时</Button> {mag} </section> ) } 复制代码非常规组件该部分,是本人在平时项目开发中,有时候会用到的组件封装思路,如果大家有好意见和建议,可以互相讨论。总之,想到达的目的就是取其精华,去其槽粕。因为在项目开发中用的UI库是Antd,有些使用方式也是基于它的使用规则去使用的。RandomKey如果你在React项目中,通过遍历一个数组来渲染一些组件,例如:li。如果你不给加一个key在控制台会有警告出现。这个key也是React中隐藏属性。是为了Diff算法用的。但是,有一些场景,比方说,一些弹窗,是需要用户填写一些信息的,而有一些用户在填写的时候,有取消的操作,常规的处理方式是不是,需要遍历,并通过对比原来的信息进行数据的恢复。而,如果你能够在组件调用处监听到用户的取消事件,那提供一个比较方便,但是这也是属于无奈之举的方式。RandomKey。<Test key={Math.random() />} 复制代码State组件这个名词是本人,擅自命名的,如果大家感觉有好的名字,在评论区告诉我。如果在React开发项目中,你前期可能规划的很好,按部就班的去写页面,但是,但是,但是,你架不住,产品每天不停的改需求,而改来改去,发现自己原先规划的东西自己都看不懂了。或者,你们老大让你去维护别人写的代码,追加一个新的需求,而当你看到别人写的代码时候,心凉了。这TM是人能看懂的代码吗,不说逻辑是否,负责,TM一个页面的所有功能都堆砌在一个文件中。(这里说堆砌一点都不为过,我看过别人写一个页面1000行代码,20多个state)。比方说,现在有如下的组件,需要新增一个弹窗,而新增一个弹窗,我们来简单的细数一下需要的变量1. visibleForXX2. valueXX暂且就按最少的来class StateComponent extends React.Component{ state={ //这里忽略7-10个参数 visibleForXX:false, valueXX:'北宸', } handleVisible=(visible)=>{ this.setState({ visibleForXX:visible }) } render( const {visibleForXX,valueXX}= this.state; const {valueFromPorpsA,valueFromPorpsB} = this.props; return ( <section> //这里是700多行的魔鬼代码 <Button onClick=(()=>this.handleVisible(true))>我叫一声你敢答应吗</Button> <Button onClick=(()=>this.handleVisible(false))>我不敢</Button> {visibleForXX? <Modal visible={visibleForXX} valueXX={valueXX} valueFromPorpsA={valueFromPorpsA} //....如果逻辑负责,可能还很多 > //bala bala 一大推 </Modal> :null } </section> ) ) } 复制代码这样写有毛病吗,一点毛病都没有,能实现功能吗,能。能交差吗,能。能评优吗,评优和代码质量有毛关系。后面维护你的代码的人,能问候你亲戚吗,我感觉也能。同时大家发现一个问题吗,虽然我在写代码的时候,特意用了解构,但是每次不管是否和该组件相关的有关的渲染,都会进行一次按作用域链查找。有没有必要,这个还没有。那有啥可以解决呢,有人会说,那你不会拆一个组件出来啊。可以,如果按我平时开发,这个新功能一般都是一个组件。但是,把上述Modal代码拆出去,其实还是会有每次render作用域链查找问题。其实,我们可以换种思考思路。直接上菜吧。import Modal from '../某个文件路径,当然也可以用别名' class StateComponent extends React.Component{ state={ //这里忽略7-10个参数 ModalNode:null } handleClick=()=>{ const {valueFromPorpsA,valueFromPorpsB} = this.props; this.setState( ModalNode:<Modal visible={visibleForXX} valueXX={valueXX} valueFromPorpsA={valueFromPorpsA} //....如果逻辑负责,可能还很多 /> ) } render( const {ModalNode}= this.state; return ( <section> //这里是700多行的魔鬼代码 <Button onClick=(this.handleClick)>我叫一声你敢答应吗</Button> {ModalNode? ModalNode :null } </section> ) ) } 复制代码把组件换一个位置,他不香吗。用最密集的代码做相关的事。虽然,有可能接收你代码的人,可能会骂街,但是等他替换一个简单的参数。就不需要在1000行代码中遨游了。上述例子,只是简单说了一下思路,但是肯定能实现,由于篇幅问题,就不CV又臭又长的代码了。自我控制显隐的Modal有没有遇到这么一个需求,需要在某一个页面中,新增一个弹窗,而这个新增的任务又双叒叕交给你了。而你欣然接受,发现需要新增的地方,又是一堆XX代码。(你懂我说的意思)我们继续分析一下常规Modal(在已经将Modal封装成一个组件前提下)具备的条件(在调用处需要准备的东西)1. visible2. this.handleVisible(false)3. this.handleOk(这里可能有额外操作)列举的,是最简单的,可能有比这还复杂的。this.handleVisible(false)是控制Modal关闭的。然后你又继续bala bala的写。如果一个页面存在1-2个modal还是可以接受,但是如果是4-5个呢,你还是用这种方式,我的天,恭喜你成为了CV俱乐部高级会员。我可以弱弱的卑微的提出两点1. 将Modal的壳子封装成组件,你只负责content的书写2. 封装的Modal自己去控制显隐我们着重说一下2,因为这个篇幅是2的主场。Talk is cheap,show your the code组件调用方式,会发现,只要引入对应的组件,并且定义一个孩子节点,就可以实现一个控制显隐的组件。<OperatingModal> <Button>点击</Button> </OperatingModal> 复制代码Modal简单实现思路import React from 'react'; import { Modal } from 'antd'; export class OperatingModal extends React.Component { static getDerivedStateFromProps(nextProps, state) { if ('visibleFromParent' in nextProps && nextProps.visibleFromParent && !state.visible) { return { visible: nextProps.visibleFromParent } } return null; } state = { visible: false } handleVisible = (visible) => { this.setState({ visible: visible }); } handleOk = (secondNode) => { //控制显示隐藏 } renderFooter = () => { const { footer, footerPosition } = this.props; let footerNode = footer; if (footer != null) { let footerChidren = footerNode.props.children; footerNode = <div > {footerChidren.length > 1 ? <React.Fragment> //克隆处理 </React.Fragment> : footer} </div> } return footerNode; } render() { const { title, children, content, width, closable, zIndex } = this.props; const { visible } = this.state; return ( <section> {children ? React.cloneElement(children, { onClick: () => this.handleVisible(true) }) : null } <Modal //常规配置 > {content} </Modal> </section> ) } } OperatingModal.defaultProps = { title: '提示', content: '我是内容', children: null,//需要包裹的孩子节点 footer: null, visibleFromParent: false, width: 300, closable: true, footerPosition: 'center', zIndex: 1000, } 复制代码这是我简单的一个版本,你如果正好用antd,你可以尝试用一下。有一点需要说明,这里用到了,一个React属性React.cloneElement。具体API讲解可以参考官网。自带form提交的Modal这个例子是基于2(自我控制显隐的Modal)的升级版本,只提供一个思路。主要代码如下。该组件具备的功能:1. form表单和非form表单2. 自控显隐3. 防重复提交4. from 提交之后,能够获取到接口结果,进行其他操作5. 父组件也可以调用显示隐藏6. ....组件调用方式<AdaptationFromModal> <From.Item> //..... </From.Item> //很多From.Item </AdaptationFromModal> 复制代码组件实现大致思路import React, { Component } from 'react'; import { Modal, Form, Button, message, } from 'antd'; import throttle from 'lodash.throttle'; class AdaptationFromModal extends Component { constructor(props, context) { super(props, context); this.state = { visible: false, isSubmitLoading: false, randomKey: 1, }; this.handleOK = this.handleOK.bind(this); this.handleOKThrottled = throttle(this.handleOK, 4000); } componentWillUnmount() { this.handleOKThrottled.cancel(); } renderFormCotent = () => { let formContentNode = null; const { formItemLayout } = this.props; formContentNode = ( <Form > //对renderCotent进行遍历 </Form> ); return formContentNode; } renderNormalContent = () => { let normalContentNode = null; normalContentNode = this.props.renderCotent(//可以将组件值抛出去); return normalContentNode; } webpackModalVisibleStatus = (visible = false) => { this.setState({ visible: visible, }, () => { //根据不同的visible处理相关的代码 }); } handleOK() { const { isIncludeForm, callbackForOK, callbackForOKAfter } = this.props; this.setState({ isSubmitLoading: true, }); if (isIncludeForm) { // 进行接口数据的处理 } } renderFooter = () => { const { isHasPersonFooter, defineFooterNode, callbackForOK, callbackForOKAfter, } = this.props; const { isSubmitLoading } = this.state; let footerNode = null; if (isHasPersonFooter) { if (defineFooterNode !== null) { footerNode = defineFooterNode(//这里可以将组件内部的值,抛出去 ); } } else { footerNode = ( //常规渲染 ); } return footerNode; } render() { const { visible, randomKey } = this.state; const { width, isIncludeForm, title, children, style, } = this.props; return ( <section style={style || {}}> {children ? React.cloneElement(children, { onClick: () => this.webpackModalVisibleStatus(true) }) : null} {visible ? ( <Modal //常规Modal配置 > { isIncludeForm ? this.renderFormCotent() : this.renderNormalContent() } </Modal> ) : null} </section> ); } } AdaptationFromModal.defaultProps = { title: 'xxx', // width: 600, isIncludeForm: true, isHasPersonFooter: false, callbackForOkClick: null, // 点击确定的回调函数 formItemLayout: { labelCol: { span: 6 }, wrapperCol: { span: 14 }, }, style: undefined, }; export default Form.create()(AdaptationFromModal); 复制代码Antd Form壳子有些公司业务中,可能有表单啊,类似的功能,就是简单的form处理。但是,antd的在进行表单处理的时候,需要很多冗余的配置。如下:<Form layout="inline" onSubmit={this.handleSubmit}> <Form.Item validateStatus={usernameError ? 'error' : ''} help={usernameError || ''}> {getFieldDecorator('username', { rules: [{ required: true, message: 'Please input your username!' }], })( <Input prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="Username" />, )} </Form.Item> <Form.Item validateStatus={passwordError ? 'error' : ''} help={passwordError || ''}> {getFieldDecorator('password', { rules: [{ required: true, message: 'Please input your Password!' }], })( <Input prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} type="password" placeholder="Password" />, )} </Form.Item> </Form> 复制代码比方说有些Form.Item,getFieldDecorator这些看起来很碍眼。是不是有一种处理方式,就是简单的配置一些页面需要的组件,而这些处理放在一个壳子里呢。<SearchBoxFactory callBackFetchTableList={this.callBackFetchTableList} callBackClearSearchCondition={this.callBackClearSearchCondition} > <Input placeholder={'请输入你的姓名'} formobj={{ apiField: 'XXA', initialValue: '', maxLength: 50, label: '北宸', }} /> <Input placeholder={'请输入你的性别'} formobj={{ apiField: 'XXB', initialValue: '', maxLength: 50, label: '南蓁', }} /> <Select style={{ width: 120 }} formobj={{ apiField: 'status', initialValue: '1', }} > <Option value="1">我帅吗?</Option> <Option value="2">那必须滴。(锦州语气)</Option> </Select> </SearchBoxFactory> 复制代码这种处理方式他不香吗。就问你香不香。总结其实React中组件化,是一个编程思路,我感觉,懒人才可以真正的领略到他的魅力,因为懒人才可以想出让自己书写代码,更快,更高,更强的方式。这也是一种编程习惯和方式。用最短的时间,写最少的代码,干最漂亮的代码。希望我啰嗦这么多,能对你有所帮助。如果大家看到有哪里不是很明白的,可以评论区,留言。如果感觉还不是很尽兴。没关系,这些东西,有很多。其实每天都在吵吵组件化,组件化,组件化是在实际开发项目中,在解决实际业务中,才会有灵感。他主要的目的是解决实际问题,而不是闭门造车。
该文章是直接翻译国外一篇文章,关于JS的Module模式的深度解析(这也是JS设计模式中的一种模式)。都是基于原文处理的,其他的都是直接进行翻译可能有些生硬,所以为了行文方便,就做了一些简单的本地化处理。如果想直接根据原文学习,可以忽略此文。同时该篇文章也算是,前端模块化的番外篇。(这篇文章也在准备当中,敬请期待) 复制代码模块模式是一种常用的代码模式。它简单实用,但是也有一些“优雅”的使用方式,没有得到开发者的重视。所以,这篇文章,带大家来重温一下基层的用法,并且介绍一些比较优雅的使用方式。译者注:模块模式,其实就是JS实现模块化的最基础的地基。例如AMD,UMD,COMMONJS,还有ES6的module都是基于这个实现方式(构建一个IIFE,{独立作用域})来实现模块化编程还有一点就是ES6中class是ES5构造函数的语法糖。ES5在自定义一个类,需要构造函数+构造函数.prototype来实现,但是为什么ES6的class却可以将prototype中的方法放在class的代码范围中。(也就是说,class一次性将构造函数和prototype都构建了。)如果想了解Class如何优雅的进行“糖化”基础用法我们来简单回顾一下什么是module pattern。如果你对基础知识比较熟悉的话,可以跳过这部分,直接翻阅"高级用法"。匿名作用域(Anonymous Closures)匿名作用域是实现模块化最基本的结构,也是在JS的语言范畴中,最好的实现方式。我们简单的构建了一个匿名函数,并且立马执行该匿名函数。在该函数中的所有代码都独立的运行在指定作用域中。并且该作用域中定义的私有变量和状态值贯穿项目的所有周期。(function () { //在该作用域中的所有变量和函数都挂载在了全局变量上(都是全局变量) }()); 复制代码Notice:在匿名函数外包还有一个()。这是必须要的。因为在JS中如果一个语句是以function开头,JS引擎会认为这是一个函数声明。而通过()包裹之后,就变成了函数表达式。导入全局变量JS语法中,存在一个很有意思的特性:隐含的全局变量。当访问一个变量名,JS编译器就会循着作用域链(scope chain)去查找是否在指定的结点中存在与之相同的变量名。如果在整条作用域链中都没有发现该变量名,这个变量就会被自动赋给全局变量。当编译器对一个原本不存在的变量进行赋值,该变量也会自动挂载在全局变量。针对隐含的全局变量这个特性,在一个匿名作用域中使用/创建一个变量是非常简单的。而这恰恰让代码变的维护性下降。幸运的是,匿名函数为我们提供了一种解决方案。通过将全局变量作为参数传入到匿名函数中,直接对传入的全局变量赋值和查值。这样就比隐含的全局变量通过作用域链查找和赋值变量的方式更快,更简洁。(function ($, YAHOO) { //在该作用域中,就能访问jQuery, YAHOO的实例了 }(jQuery, YAHOO)); 复制代码模块导出有些应用场景中,不仅仅是用到全局变量,而且还想声明一个全局变量。我们可以通过在匿名函数中return一个对象,来实现声明全局变量。var MODULE = (function () { var my = {}, privateVariable = 1; function privateMethod() { // ... } my.moduleProperty = 1; my.moduleMethod = function () { // ... }; return my; }()); 复制代码Notice:我们声明了一个名为MODULE的全局模块,该模块拥有两个公共(public)属性:一个方法(MODULE.moduleMethod)、一个变量(MODULE.moduleProperty) 并且该模块通过匿名函数实现了私有的(private)变量和方法。高级用法尽管上面的简单用法,能满足我们90%的模块需求,但是我们可以基于普通用法,来构建更加高级的用法追加属性和方法(Augmentation)针对上述模块实现而言,存在一个弊端/限制,就是一个文件定义整个模块的实现。 针对大型项目而言,代码的布局的高内聚,低耦合很重要。所以,有些特定的实现是不需要都堆砌在一个文件中的。而argment modules这种代码布局方式就应运而生。1. 导入指定模块2. 在指定模块中新增属性3. 导出修饰之后的模块var MODULE = (function (my) { //基于MODULE的基础上,新增指定方法/属性 my.anotherMethod = function () { }; return my; }(MODULE)); 复制代码在该匿名函数被执行之后,原先的module就会新增了一个新的公共方法(MODULE.anotherMethod)。该文件也可以存在自己的私有方法等。宽松的扩展(Loose Augmentation)我们上述的例子中,要求我们先构建一个初始模块,然后进行追加操作。其实这种处理方式不是必要的。因为,</code>标签可以实现异步加载,这样的话,就不存在模块初始化的问题,可能追加的模块先加载。这样就不会存在<strong>初始模块</strong>这个概念。</div><div data-lake-id="8503778d930e8ba629d6022a16c7c322">所以,我们需要一种定义模块的方式,而这种方式是不关心各个模块的加载顺序。</div><div data-lake-id="d2a9991225e915cfb79ef781c283e15e">Talk is cheap ,show you the code:</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22var%20MODULE%20%3D%20(function%20(my)%20%7B%5Cn%5Ct%2F%2F%20%E9%9A%8F%E6%84%8F%E6%96%B0%E5%A2%9E%E5%B1%9E%E6%80%A7%5Cn%5Ctreturn%20my%3B%5Cn%7D(MODULE%20%7C%7C%20%7B%7D))%3B%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22HLx8p%22%7D"></div><div data-lake-id="0c5c90f9b0562325143c0f63d38e9712">Notice:</div><blockquote style="background-color: #F8F8F8;"><div data-lake-id="847cb9ec93d0320b4e19e2b2292ceda0">1.在该中模式下,<code>var</code>的声明是必要的。</div><div data-lake-id="85406308d09dd4373527428ae69a8217">2. 导入的模块是不需要考虑先前是否存在。也就意味着,使用<code>Loose Augmentation</code>构建的模块,在调用的时候,可以利用类似于<code>LABjs</code>的工具库,实现<strong>平行</strong>加载。</div></blockquote><h2 data-lake-id="2336c51a1f409be68b4d6c99fcf0cefc" id="OfYNO"><br /></h2><h2 data-lake-id="0ea40eb688ad1d1365cf6e4de3662ea9" id="Jco7p">严谨的扩展(Tight Augmentation)</h2><div data-lake-id="e642c88f1d634da431a64cc05b9e662f"><br /></div><div data-lake-id="63f716c29f4f3b96aea78e3d2fd845d5">虽然利用<code>loose augmentation</code>构建的模块很好,但是也对模块新增了一些约束。其中比较重要的就是,1.你无法<strong>安全</strong>的对模块中的属性和方法进行<strong>重写</strong>。2.在初始化的时候,是无法使用在另外一个文件中定义的模块的属性。</div><div data-lake-id="1a79d958c6179d37e237b99767f9f5be">而<code>Tight augmentation</code>隐藏了加载顺序。但是允许进行方法和属性的<strong>重载(override)</strong> 我们将原先实现过的<code>MODULE</code>作为参数传入到函数中</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22var%20MODULE%20%3D%20(function%20(my)%20%7B%5Cn%5Ctvar%20old_moduleMethod%20%3D%20my.moduleMethod%3B%5Cn%20%20%20%20%2F%2F%E8%BF%9B%E8%A1%8C%E6%96%B9%E6%B3%95%E7%9A%84%E9%87%8D%E6%96%B0%EF%BC%8C%E4%BD%86%E6%98%AF%E5%8F%AF%E4%BB%A5%E9%80%9A%E8%BF%87old_moduleMethod%E8%AE%BF%E9%97%AE%E5%8E%9F%E6%9D%A5%E7%9A%84%E6%96%B9%E6%B3%95%5Cn%5Ctmy.moduleMethod%20%3D%20function%20()%20%7B%5Cn%5Ct%5Ct%2F%2F%20...%5Cn%5Ct%7D%3B%5Cn%5Ctreturn%20my%3B%5Cn%7D(MODULE))%3B%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22EbeKB%22%7D"></div><div data-lake-id="0178aad0f83521bd4f61fc97343817d1">上述代码中,我们即对<code>MODULE.moduleMethod</code>进行<strong>重写</strong>,同时保持了对原始方法的<strong>引用</strong>(如果有必要的话)。</div><h2 data-lake-id="6df34f743adadd58f3d2d0ed4d062742" id="u2Pth"><br /></h2><h2 data-lake-id="731575fcaaec7571331a80ce0c295490" id="N9MTY">复制和继承(Cloning and Inheritance)</h2><div data-lake-id="3f164352d9614c950150f2a296f51cec"><br /></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22var%20MODULE_TWO%20%3D%20(function%20(old)%20%7B%5Cn%5Ctvar%20my%20%3D%20%7B%7D%2C%5Cn%5Ct%5Ctkey%3B%5Cn%5Ctfor%20(key%20in%20old)%20%7B%5Cn%5Ct%5Ctif%20(old.hasOwnProperty(key))%20%7B%5Cn%5Ct%5Ct%5Ctmy%5Bkey%5D%20%3D%20old%5Bkey%5D%3B%5Cn%5Ct%5Ct%7D%5Cn%5Ct%7D%5Cn%5Ctvar%20super_moduleMethod%20%3D%20old.moduleMethod%3B%5Cn%5Ctmy.moduleMethod%20%3D%20function%20()%20%7B%5Cn%5Ct%5Ct%2F%2F%E9%87%8D%E6%96%B0%E5%A4%8D%E5%88%B6%E4%B9%8B%E5%90%8E%E7%9A%84%E6%96%B9%E6%B3%95%EF%BC%8C%E9%80%9A%E8%BF%87super_moduleMethod%E6%9D%A5%E8%AE%BF%E9%97%AE%E5%8E%9F%E5%A7%8B%E6%96%B9%E6%B3%95%5Cn%5Ct%7D%3B%5Cn%5Ctreturn%20my%3B%5Cn%7D(MODULE))%3B%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22e2sJN%22%7D"></div><div data-lake-id="5567bc92326e02d99c6d4e0d2113fd0e">该实现方式,可能是最<strong>灵活</strong>的选择。</div><h2 data-lake-id="39093574fcfd4125f4863afe055e7328" id="Sg0xp"><br /></h2><h2 data-lake-id="1b7de103c6321891a7fe5b8ab2d8ea44" id="Y3cEI">跨文件的私有变量(Cross-File Private State)</h2><div data-lake-id="256b8619bcf2b178d2da726afb952845"><br /></div><div data-lake-id="a445ba192265095e18ea9a7a1b445719">将一个模块分成很多文件组成最大的限制就是:每个文件拥有自己的私有变量,同时这些私有变量无法跨文件访问。这样就无法进行单一模块的拆分处理。</div><div data-lake-id="d5b29461be6d145943450aeae149fa61">但是,利用<code>loosely augmented module</code>可以很好的解决这个问题:</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22var%20MODULE%20%3D%20(function%20(my)%20%7B%5Cn%5Ctvar%20_private%20%3D%20my._private%20%3D%20my._private%20%7C%7C%20%7B%7D%2C%5Cn%5Ct%5Ct_seal%20%3D%20my._seal%20%3D%20my._seal%20%7C%7C%20function%20()%20%7B%5Cn%5Ct%5Ct%5Ctdelete%20my._private%3B%5Cn%5Ct%5Ct%5Ctdelete%20my._seal%3B%5Cn%5Ct%5Ct%5Ctdelete%20my._unseal%3B%5Cn%5Ct%5Ct%7D%2C%5Cn%5Ct%5Ct_unseal%20%3D%20my._unseal%20%3D%20my._unseal%20%7C%7C%20function%20()%20%7B%5Cn%5Ct%5Ct%5Ctmy._private%20%3D%20_private%3B%5Cn%5Ct%5Ct%5Ctmy._seal%20%3D%20_seal%3B%5Cn%5Ct%5Ct%5Ctmy._unseal%20%3D%20_unseal%3B%5Cn%5Ct%5Ct%7D%3B%5Cn%5Ct%2F%2F%20permanent%20access%20to%20_private%2C%20_seal%2C%20and%20_unseal%5Cn%5Ctreturn%20my%3B%5Cn%7D(MODULE%20%7C%7C%20%7B%7D))%3B%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22dwwiC%22%7D"></div><div data-lake-id="5a55b5d28562e7b4d96b3e8e34daf1b2">任何文件都可以在<strong>局部变量</strong>(<code>_private</code>)上设置属性,并且在其他文件中可以<strong>立马</strong>范围到。</div><div data-lake-id="0f79a027cd132040c6c323b4b0b50835">一旦该模块<strong>加载</strong>完成,程序调用<code>MODULE._seal()</code>,用于阻止外部文件访问该模块的内部属性(<code>internal _private</code>)。</div><div data-lake-id="66fa2b6b1d8cc5af25a297b5eea68600">如果需要对该模块进行扩展,则在应用程序的生命周期中任何文件下的内部方法中在新模块加载之前调用<code>_unseal()</code>。在扩展之后,继续调用<code>_seal()</code>用于私有属性的加密处理。</div><h2 data-lake-id="fc9ed698f91c46c85aeaeaab763c3dd5" id="kQ4r5"><br /></h2><h2 data-lake-id="e9178812bef0b7abcc7f205ae18ca627" id="ptJss">子模块(Sub-modules)</h2><div data-lake-id="82c7fb149f0e3856f0b9537893f58e67"><br /></div><div data-lake-id="a949a7fb4846d6664caab5453562cb70">我们上述介绍的高级模块都很简单。同时也有很多构建一个<strong>子模块</strong>的方式。</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22MODULE.sub%20%3D%20(function%20()%20%7B%5Cn%5Ctvar%20my%20%3D%20%7B%7D%3B%5Cn%5Ct%2F%2F%20...%5Cn%5Ctreturn%20my%3B%5Cn%7D())%3B%5Cn%E5%A4%8D%E5%88%B6%E4%BB%A3%E7%A0%81%22%2C%22id%22%3A%22cI2dI%22%7D"></div><h1 data-lake-id="91e8b512e36318058e409cd8aae667dd" id="59q7A"><br /></h1><h1 data-lake-id="ba580a0669739edf109d4854ca247160" id="qKbTs">总结</h1><div data-lake-id="071dd92cc35831c841c33be5377536c1"><br /></div><div data-lake-id="33e1f545d116e3cb1e6ea0ca923030c0">大多数的高级模式都可以互相组合用于构建一个更加方便的模式。如果想要构建一个比较复杂的程序。可以尝试<strong>loose augmentation</strong>、<strong>private state</strong>、 和 <strong>sub-modules</strong>的组合。</div>
该文章是直接翻译国外一篇文章,关于JS原型。都是基于原文处理的,其他的都是直接进行翻译可能有些生硬,所以为了行文方便,就做了一些简单的本地化处理。如果想直接根据原文学习,可以忽略此文。全新对象在JS中,对象是有很多key和value构成的一种数据存储结构。例如,如果想描述一个人的基本信息,可以构建一个拥有firstName和lastName的对象,并且分别被赋值为北宸和范。在JS对象中的key的值是String类型的。在JS中,可以用Object.create创建一下全新的对象://构建了一个空对象 var person = Obeject.create(null); 复制代码此时有些开发会说,为什么不用var person ={} 来构建一个空对象。其实之所以能用这种处理方式,只是JS引擎给你做了处理。为了能够用最原始的代码去了解原型,我们也需要循序渐进的去接触这些东西。如果通过一个key遍历对象,但是对象中没有对应的key能进行匹配,JS就会返回一个 undefined。person["name"] //undefined 复制代码如果key是一个合法的标识符,也可以用如下的语法进行对象数据的访问:person.name //undefined 复制代码合法的标识符格式:in general, an identifier starts with a unicode letter, $, _, followed by any of the starting characters or numbers. A valid identifier must also not be a reserved word. There are other allowed characters, such as unicode combining marks, unicode connecting punctuation, and unicode escape sequences.给对象赋值现在你已经有了一个空对象,但是好像并没有啥卵用。在我们为对象新增自定义属性之前,我们还需要对对象的额外属性(named data property)有一个更深的了解。一般而言,对象的自定义属性有一个name属性和与name属性相对应的value属性。但是自定义属性还可以被enumerable、configurable、writable这些隐藏属性控制,来在不同的场景中表现出不同的行为。如果一个自定义属性是enumerable的,通过for(prop in obj)来操作该自定义属性的宿主对象,这个自定义属性会被循环操作捕获。 如果是writable的,你可以对这个自定义属性进行赋值。如果是configurable的,可以对这个自定义属性进行删除或者是改变其他的隐藏属性。一般在我们创建一个新的自定义属性,我们总是希望这个属性是enumerable、writable、configurable的。我们可以利用Object.defineProperty来向对象新增一个自定义属性。var person = Object.create(null); Object.defineProperty(person, 'firstName', { value: "Yehuda", writable: true, enumerable: true, configurable: true }); Object.defineProperty(person, 'lastName', { value: "Katz", writable: true, enumerable: true, configurable: true }); 复制代码这样新增属性代码有些冗余,我们可以将配置信息提出来:var config = { writable: true, enumerable: true, configurable: true }; var defineProperty = function(obj, name, value) { config.value = value; Object.defineProperty(obj, name, config); } var person = Object.create(null); defineProperty(person, 'firstName', "Yehuda"); defineProperty(person, 'lastName', "Katz"); 复制代码虽然代码有一些精简了,但是还是看起来很别扭。所以我们需要一个更加合理或者是更加简洁的方式来定义属性。Prototypes从上面我们得知,JS中的对象就是一系列key和对应value组成的数据格式。但是在JS中还存在一个属性(一个指向其他对象的指针)。我们称这个指针为对象的原型(prototype)。如果你在某个对象中,查找一个key,但是在该对象范围内没有找到。JS会继续在prototype所指向的对象中继续寻找。以此类推,直到prototype所指向的对象是一个null,查询结束。并且返回undefined。让我们来回顾一下,在调用Object.create(null)的时候,会发生些什么。 方法中接收一个null为参数。其实也就是说,在利用Object.create(paramsObj)构建出的对象,他的prototype指向了paramsObj。可以通过Object.getPrototyOf来查询指定对象的prototype.var man = Object.create(null); defineProperty(man, 'sex', "male"); var yehuda = Object.create(man); defineProperty(yehuda, 'firstName', "Yehuda"); defineProperty(yehuda, 'lastName', "Katz"); yehuda.sex // "male" yehuda.firstName // "Yehuda" yehuda.lastName // "Katz" Object.getPrototypeOf(yehuda) // returns the man object 复制代码我们可以通过这种方式来新增函数,这样这个函数就会被多处使用:var person = Object.create(null); defineProperty(person, 'fullName', function() { return this.firstName + ' ' + this.lastName; }); //将man的prototype指向person ,这样,在man对象还有已man为prototype的对象都可以访问这个函数 var man = Object.create(person); defineProperty(man, 'sex', "male"); var yehuda = Object.create(man); defineProperty(yehuda, 'firstName', "Yehuda"); defineProperty(yehuda, 'lastName', "Katz"); yehuda.sex // "male" yehuda.fullName() // "Yehuda Katz" 复制代码设置属性由于构建一个具有writable、configurable、enumerable属性的新对象很常见。所以JS为了让代码看起来简洁,采用另外一种给对象新增属性的方式。通过简化方式,让代码看起来很短,也不需要额外的处理writable、configurable、enumerable等属性,同时这些属性的值都是true。var person = Object.create(null); //在此处我们可以直接定义想要给对象新增的属性,而writable, // configurable, 和 enumerable这些属性由JS统一处理 person['fullName'] = function() { return this.firstName + ' ' + this.lastName; }; //将man的prototype指向person ,这样,在man对象还有已man为prototype的对象都可以访问这个函数 var man = Object.create(person); man['sex'] = "male"; var yehuda = Object.create(man); yehuda['firstName'] = "Yehuda"; yehuda['lastName'] = "Katz"; yehuda.sex // "male" yehuda.fullName() // "Yehuda Katz" 复制代码对象字面量JS提供了一个种字面量语法来构建一个对象。同时可以一次性将所有需要新增的属性都指出并赋初值。var person ={ firstName: "北宸", lastName: "范" } 复制代码其实上面的实现是如下代码的"语法糖":var person = Object.create(Object.prototype); person.firstName = "北宸"; person.lastName = "范"; 复制代码person的原型链为Object.prototype。这个可以在控制台中实践一下Object.prototype对象中包含了很多我们希望在定义的对象中可以通过原型链访问的方法和属性。通过上面的分析可以得知,我们通过字面量构建的对象,它的原型就是Object.prototype。当然,我们也有机会在定义的对象中对存储在Object.prototype中的方法进行重写。var alex = { firstName: "Alex", lastName: "Russell" }; alex.toString() // "[object Object]" var brendan = { firstName: "北宸", lastName: "范", toString: function() { return "范北宸"; } }; brendan.toString() // "范北宸" 复制代码但是基于字面量构建对象的原型是无法进行指定的。也就是说,字面量构建的对象的原型永远都是Object.prototype。这样做的话,就无法利用原型来分享共有属性和方法。所以,我们需要对这种模式进行改进。var fromPrototype = function(prototype, object) { //用于将自定义的原型和目标对象进行关联,这样在新的对象中就可以访问原型中的方法和属性(原型搭建) var newObject = Object.create(prototype); //遍历目标对象,将属于目标对象中的属性都复制到新对象中,(属性迁移) for (var prop in object) { if (object.hasOwnProperty(prop)) { newObject[prop] = object[prop]; } } return newObject; }; var person = { toString: function() { return this.firstName + ' ' + this.lastName; } }; var man = fromPrototype(person, { sex: "male" }); var jeremy = fromPrototype(man, { firstName: "北宸", lastName: "范" }); jeremy.sex // "male" jeremy.toString() // "范北宸" 复制代码如上的对象构建的对象结构如下:基于原型的面向对象编程有一点很明确,原型(prototype)可以用于继承(继承是OOP语言最明显的特点之一)。为了利用原型来实现继承,JS提供了new操作符。为了实现面向对象编程,JS允许你使用一个函数对象将prototype和constructor封装起来。//Person的constructor var Person = function(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } //Person的prototype Person.prototype = { toString: function() { return this.firstName + ' ' + this.lastName; } } 复制代码自此,我们就实现了一个用于作为constructor的函数对象还有作为新对象的prototype的对象。让我们通过构建一个函数来模拟new的操作流程。它的主要目的就是为了新建指定的构造函数的实例.function newObject(func) { // 构建函数的参数list var args = Array.prototype.slice.call(arguments, 1); // 基于构造函数的原型构建一个对象 var object = Object.create(func.prototype); // 由于构造函数中存在this的值,所以在构建实例的时候,需要将this的指向实例对象 func.apply(object, args); // 返回基于指定构造函数构建的新对象 return object; } var brendan = newObject(Person, "范", "北宸"); brendan.toString() // "范北宸" 复制代码Note:这里func.apply(object, args)的操作有一个this指向问题。可以参考理解JS函数调用和"this"在JS实际构建对象中,用的是new。var mark = new Person("范", "北宸"); mark.toString() // "范北宸" 复制代码Note:关于new的运行机制,大致如下:创建一个空对象,作为将要返回的对象实例。将这个空对象的原型,指向构造函数的prototype属性。将这个空对象赋值给函数内部的this关键字。开始执行构造函数内部的代码。本质上,JS函数中的"class"其实就是一个函数对象作为构造函数同时附带上一个prototype对象。
2023年04月