2FPS

2FPS 查看完整档案

杭州编辑  |  填写毕业院校  |  填写所在公司/组织 www.zhuyuntao.cn 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

2FPS 赞了文章 · 2020-05-06

「吐血整理」再来一打Webpack面试题

从头发的浓密程度和干练的走路姿势我察觉到,面前坐着的这位面试官也是一把好手。

我像以往一样,准备花3分钟的时间进行自我介绍。在此期间,我的目光被16寸的MacBook Pro所吸引,这次的自我介绍我做足了准备,很有信心征服面试官。不出我所料,面试官被我引入了我擅长的领域。

看来你对Webpack很熟悉,那我来考考你

0.有哪些常见的Loader?你用过哪些Loader?

(我开始熟悉的报起了菜名)

  • raw-loader:加载文件原始内容(utf-8)
  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)
  • url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给file-loader处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)
  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试
  • svg-inline-loader:将压缩后的 SVG 内容注入代码中
  • image-loader:加载并且压缩图片文件
  • json-loader 加载 JSON 文件(默认包含)
  • handlebars-loader: 将 Handlebars 模版编译成函数并返回
  • babel-loader:把 ES6 转换成 ES5
  • ts-loader: 将 TypeScript 转换成 JavaScript
  • awesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader
  • sass-loader:将SCSS/SASS代码转换成CSS
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
  • postcss-loader:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀
  • eslint-loader:通过 ESLint 检查 JavaScript 代码
  • tslint-loader:通过 TSLint检查 TypeScript 代码
  • mocha-loader:加载 Mocha 测试用例的代码
  • coverjs-loader:计算测试的覆盖率
  • vue-loader:加载 Vue.js 单文件组件
  • i18n-loader: 国际化
  • cache-loader: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里

更多 Loader 请参考官网

(面试官:挺好,知道的还挺多)

1.有哪些常见的Plugin?你用过哪些Plugin?

(这大兄弟好像听上瘾了,继续开启常规操作)

  • define-plugin:定义环境变量 (Webpack4 之后指定 mode 会自动配置)
  • ignore-plugin:忽略部分文件
  • html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)
  • web-webpack-plugin:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用
  • uglifyjs-webpack-plugin:不支持 ES6 压缩 (Webpack4 以前)
  • terser-webpack-plugin: 支持压缩 ES6 (Webpack4)
  • webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度
  • mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)
  • serviceworker-webpack-plugin:为网页应用增加离线缓存功能
  • clean-webpack-plugin: 目录清理
  • ModuleConcatenationPlugin: 开启 Scope Hoisting
  • speed-measure-webpack-plugin: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时)
  • webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)

更多 Plugin 请参考官网

(Double Kill)

2.那你再说一说Loader和Plugin的区别?

(就知道你会问这个,我用手掩盖着嘴角的微笑)

Loader 本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。
因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。

Plugin 就是插件,基于事件流框架 Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

Loader 在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。

Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。

3.Webpack构建流程简单说一下

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  • 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  • 确定入口:根据配置中的 entry 找出所有的入口文件
  • 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

简单说

  • 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
  • 编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  • 输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中

对源码感兴趣的同学可以移步我的另一篇专栏从源码窥探Webpack4.x原理

4.使用webpack开发时,你用过哪些可以提高效率的插件?

(这道题还蛮注重实际,用户的体验还是要从小抓起的)

  • webpack-dashboard:可以更友好的展示相关打包信息。
  • webpack-merge:提取公共配置,减少重复配置代码
  • speed-measure-webpack-plugin:简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。
  • size-plugin:监控资源体积变化,尽早发现问题
  • HotModuleReplacementPlugin:模块热替换

5.source map是什么?生产环境怎么用?

source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。

map文件只要不打开开发者工具,浏览器是不会加载的。

线上环境一般有三种处理方案:

  • hidden-source-map:借助第三方错误监控平台 Sentry 使用
  • nosources-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高
  • sourcemap:通过 nginx 设置将 .map 文件只对白名单开放(公司内网)

注意:避免在生产中使用 inline-eval-,因为它们会增加 bundle 体积大小,并降低整体性能。

6.模块打包原理知道吗?

Webpack 实际上为每个模块创造了一个可以导出和导入的环境,本质上并没有修改
代码的执行逻辑,代码执行顺序与模块加载顺序也完全一致。

7.文件监听原理呢?

在发现源码发生变化时,自动重新构建出新的输出文件。

Webpack开启监听模式,有两种方式:

  • 启动 webpack 命令时,带上 --watch 参数
  • 在配置 webpack.config.js 中设置 watch:true

缺点:每次需要手动刷新浏览器

原理:轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout 后再执行。

module.export = {
    // 默认false,也就是不开启
    watch: true,
    // 只有开启监听模式时,watchOptions才有意义
    watchOptions: {
        // 默认为空,不监听的文件或者文件夹,支持正则匹配
        ignored: /node_modules/,
        // 监听到变化发生后会等300ms再去执行,默认300ms
        aggregateTimeout:300,
        // 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
        poll:1000
    }
}

8.说一下 Webpack 的热更新原理吧

(敲黑板,这道题必考)

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

HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。

后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loadervue-loader 都是借助这些 API 实现 HMR。

细节请参考Webpack HMR 原理解析

(面试官:不错不错,小伙子表达能力不错)

(基操,勿6)

9.如何对bundle体积进行监控和分析?

VSCode 中有一个插件 Import Cost 可以帮助我们对引入模块的大小进行实时监测,还可以使用 webpack-bundle-analyzer 生成 bundle 的模块组成图,显示所占体积。

bundlesize 工具包可以进行自动化资源体积监控。

10.文件指纹是什么?怎么用?

文件指纹是打包后输出的文件名的后缀。

  • Hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改
  • Chunkhash:和 Webpack 打包的 chunk 有关,不同的 entry 会生出不同的 chunkhash
  • Contenthash:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变

JS的文件指纹设置

设置 output 的 filename,用 chunkhash。

module.exports = {
    entry: {
        app: './scr/app.js',
        search: './src/search.js'
    },
    output: {
        filename: '[name][chunkhash:8].js',
        path:__dirname + '/dist'
    }
}

CSS的文件指纹设置

设置 MiniCssExtractPlugin 的 filename,使用 contenthash。

module.exports = {
    entry: {
        app: './scr/app.js',
        search: './src/search.js'
    },
    output: {
        filename: '[name][chunkhash:8].js',
        path:__dirname + '/dist'
    },
    plugins:[
        new MiniCssExtractPlugin({
            filename: `[name][contenthash:8].css`
        })
    ]
}

图片的文件指纹设置

设置file-loader的name,使用hash。

占位符名称及含义

  • ext 资源后缀名
  • name 文件名称
  • path 文件的相对路径
  • folder 文件所在的文件夹
  • contenthash 文件的内容hash,默认是md5生成
  • hash 文件内容的hash,默认是md5生成
  • emoji 一个随机的指代文件内容的emoj
const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        filename:'bundle.js',
        path:path.resolve(__dirname, 'dist')
    },
    module:{
        rules:[{
            test:/\.(png|svg|jpg|gif)$/,
            use:[{
                loader:'file-loader',
                options:{
                    name:'img/[name][hash:8].[ext]'
                }
            }]
        }]
    }
}

11.在实际工程中,配置文件上百行乃是常事,如何保证各个loader按照预想方式工作?

可以使用 enforce 强制执行 loader 的作用顺序,pre 代表在所有正常 loader 之前执行,post 是所有 loader 之后执行。(inline 官方不推荐使用)

12.如何优化 Webpack 的构建速度?

(这个问题就像能不能说一说从URL输入到页面显示发生了什么一样)

(我只想说:您希望我讲多长时间呢?)

(面试官:。。。)

  • 使用高版本的 Webpack 和 Node.js
  • 多进程/多实例构建:HappyPack(不维护了)、thread-loader
  • 压缩代码

    • 多进程并行压缩

      • webpack-paralle-uglify-plugin
      • uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
      • terser-webpack-plugin 开启 parallel 参数
    • 通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。
  • 图片压缩

    • 使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式)
    • 配置 image-webpack-loader
  • 缩小打包作用域

    • exclude/include (确定 loader 规则范围)
    • resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
    • resolve.mainFields 只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
    • resolve.extensions 尽可能减少后缀尝试的可能性
    • noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
    • IgnorePlugin (完全排除模块)
    • 合理使用alias
  • 提取页面公共资源

    • 基础包分离:

      • 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
      • 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
  • DLL

    • 使用 DllPlugin 进行分包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间。
    • HashedModuleIdsPlugin 可以解决模块数字id问题
  • 充分利用缓存提升二次构建速度

    • babel-loader 开启缓存
    • terser-webpack-plugin 开启缓存
    • 使用 cache-loader 或者 hard-source-webpack-plugin
  • Tree shaking

    • 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率
    • 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
    • 使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码

      • purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)
  • Scope hoisting

    • 构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
    • 必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法
  • 动态Polyfill

    • 建议采用 polyfill-service 只给用户返回需要的polyfill,社区维护。 (部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill)

更多优化请参考官网-构建性能

13.你刚才也提到了代码分割,那代码分割的本质是什么?有什么意义呢?

代码分割的本质其实就是在源代码直接上线打包成唯一脚本main.bundle.js这两种极端方案之间的一种更适合实际场景的中间状态。

阿卡丽:荣耀剑下取,均衡乱中求

用可接受的服务器性能压力增加来换取更好的用户体验。

源代码直接上线:虽然过程可控,但是http请求多,性能开销大。

打包成唯一脚本:一把梭完自己爽,服务器压力小,但是页面空白期长,用户体验不好。

(Easy peezy right)

14.是否写过Loader?简单描述一下编写loader的思路?

Loader 支持链式调用,所以开发上需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事情。

Loader的API 可以去官网查阅

  • Loader 运行在 Node.js 中,我们可以调用任意 Node.js 自带的 API 或者安装第三方模块进行调用
  • Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串,当某些场景下 Loader 处理二进制文件时,需要通过 exports.raw = true 告诉 Webpack 该 Loader 是否需要二进制数据
  • 尽可能的异步化 Loader,如果计算量很小,同步也可以
  • Loader 是无状态的,我们不应该在 Loader 中保留状态
  • 使用 loader-utils 和 schema-utils 为我们提供的实用工具
  • 加载本地 Loader 方法

    • Npm link
    • ResolveLoader

15.是否写过Plugin?简单描述一下编写Plugin的思路?

webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在特定的阶段钩入想要添加的自定义功能。Webpack 的 Tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。

Plugin的API 可以去官网查阅

  • compiler 暴露了和 Webpack 整个生命周期相关的钩子
  • compilation 暴露了与模块和依赖有关的粒度更小的事件钩子
  • 插件需要在其原型上绑定apply方法,才能访问 compiler 实例
  • 传给每个插件的 compiler 和 compilation对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件
  • 找出合适的事件点去完成想要的功能

    • emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(emit 事件是修改 Webpack 输出资源的最后时机)
    • watch-run 当依赖的文件发生变化时会触发
  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住

16.聊一聊Babel原理吧

大多数JavaScript Parser遵循 estree 规范,Babel 最初基于 acorn 项目(轻量级现代 JavaScript 解析器)
Babel大概分为三大部分:

  • 解析:将代码转换成 AST

    • 词法分析:将代码(字符串)分割为token流,即语法单元成的数组
    • 语法分析:分析token流(上面生成的数组)并生成 AST
  • 转换:访问 AST 的节点进行变换操作生产新的 AST

    • Taro就是利用 babel 完成的小程序语法转换
  • 生成:以新的 AST 为基础生成代码

想了解如何一步一步实现一个编译器的同学可以移步 Babel 官网曾经推荐的开源项目
the-super-tiny-compiler

面试官:(我听的口渴了,咱们休息一会,一会进行下半场)

面试官拿起旁边已经凉透的龙井,喝了一口。

(这小伙子有点东西)

持续更新……

参考

  • 深入浅出 Webpack
  • Webpack 实战
  • 玩转 Webpack

❤️爱心三连击

1.看到这里了就点个赞支持下吧,你的是我创作的动力。

2.关注公众号前端食堂,你的前端食堂,记得按时吃饭

3.本文已收录在前端食堂Githubgithub.com/Geekhyt,求个小星星,感谢Star。

image

查看原文

赞 78 收藏 55 评论 3

2FPS 提出了问题 · 2020-03-06

如何使用eslint同时对js和ts进行校验?

一个已经开发了几个月的项目,想转typescript,但前面的js代码不可能一下子全部改掉的,原先eslint已经支持对js的校验了,现在想增加对ts校验的支持。

目前情况是,可以做到对js或ts单种文件的支持,但如何同时支持?

关注 1 回答 1

2FPS 赞了文章 · 2019-12-18

Promise 使用、原理以及实现过程

1.什么是 Promise

promise 是目前 JS 异步编程的主流解决方案,遵循 Promises/A+ 方案。

2.Promise 原理简析

(1)promise 本身相当于一个状态机,拥有三种状态

  • pending
  • fulfilled
  • rejected
    一个 promise 对象初始化时的状态是 pending,调用了 resolve 后会将 promise 的状态扭转为 fulfilled,调用 reject 后会将 promise 的状态扭转为 rejected,这两种扭转一旦发生便不能再扭转该 promise 到其他状态。

(2)promise 对象原型上有一个 then 方法,then 方法会返回一个新的 promise 对象,并且将回调函数 return 的结果作为该 promise resolve 的结果,then 方法会在一个 promise 状态被扭转为 fulfilled 或 rejected 时被调用。then 方法的参数为两个函数,分别为 promise 对象的状态被扭转为 fulfilled 和 rejected 对应的回调函数

3.Promise 如何使用

构造一个 promise 对象,并将要执行的异步函数传入到 promise 的参数中执行,并且在异步执行结束后调用 resolve( ) 函数,就可以在 promise 的 then 方法中获取到异步函数的执行结果

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve()
  }, 1000)
}).then(
  res => {},
  err => {}
)

同时在 Promise 还为我们实现了很多方便使用的方法:

  • Promise.resolve

Promise.resolve 返回一个 fulfilled 状态的 promise

const a = Promise.resolve(1)
a.then(
  res => {
    // res = 1
  },
  err => {}
)
  • Promise.all

Promise.all 接收一个 promise 对象数组作为参数,只有全部的 promise 都已经变为 fulfilled 状态后才会继续后面的处理。Promise.all 本身返回的也是一个 promise

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise1')
  }, 100)
})
const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise2')
  }, 100)
})
const promises = [promise1, promise2]

Promise.all(promises).then(
  res => {
    // promises 全部变为 fulfilled 状态的处理
  },
  err => {
    // promises 中有一个变为 rejected 状态的处理
  }
)
  • Promise.race

Promise.race 和 Promise.all 类似,只不过这个函数会在 promises 中第一个 promise 的状态扭转后就开始后面的处理(fulfilled、rejected 均可)

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise1')
  }, 100)
})
const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise2')
  }, 1000)
})
const promises = [promise1, promise2]

Promise.race(promises).then(
  res => {
    // 此时只有 promise1 resolve 了,promise2 仍处于 pending 状态
  },
  err => {}
)

配合 async await 使用

现在的开发场景中我们大多会用 async await 语法糖来等待一个 promise 的执行结果,使代码的可读性更高。async 本身是一个语法糖,将函数的返回值包在一个 promise 中返回。

// async 函数会返回一个 promise
const p = async function f() {
  return 'hello world'
}
p.then(res => console.log(res)) // hello world

开发技巧

在前端开发上 promise 大多被用来请求接口,Axios 库也是开发中使用最频繁的库,但是频繁的 try catch 扑捉错误会让代码嵌套很严重。考虑如下代码的优化方式

const getUserInfo = async function() {
  return new Promise((resolve, reject) => {
    // resolve() || reject()
  })
}
// 为了处理可能的抛错,不得不将 try catch 套在代码外边,一旦嵌套变多,代码可读性就会急剧下降
try {
  const user = await getUserInfo()
} catch (e) {}

好的处理方法是在异步函数中就将错误 catch,然后正常返回,如下所示 👇

const getUserInfo = async function() {
  return new Promise((resolve, reject) => {
    // resolve() || reject()
  }).then(
    res => {
      return [res, null] // 处理成功的返回结果
    },
    err => {
      return [null, err] // 处理失败的返回结果
    }
  )
}

const [user, err] = await getUserInfo()
if (err) {
  // err 处理
}

// 这样的处理是不是清晰了很多呢

4.Promise 源码实现

知识的学习需要知其然且知其所以然,所以通过一点点实现的一个 promise 能够对 promise 有着更深刻的理解。

(1)首先按照最基本的 promise 调用方式实现一个简单的 promise (基于 ES6 规范编写),假设我们有如下调用方式

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 1000)
})
  .then(
    res => {
      console.log(res)
      return 2
    },
    err => {}
  )
  .then(
    res => {
      console.log(res)
    },
    err => {}
  )

我们首先要实现一个 Promise 的类,这个类的构造函数会传入一个函数作为参数,并且向该函数传入 resolve 和 reject 两个方法。
初始化 Promise 的状态为 pending。

class MyPromise {
  constructor(executor) {
    this.executor = executor
    this.value = null
    this.status = 'pending'

    const resolve = value => {
      if (this.status === 'pending') {
        this.value = value          // 调用 resolve 后记录 resolve 的值
        this.status = 'fulfilled'   // 调用 resolve 扭转 promise 状态
      }
    }

    const reject = value => {
      if (this.status === 'pending') {
        this.value = value          // 调用 reject 后记录 reject 的值
        this.status = 'rejected'    // 调用 reject 扭转 promise 状态
      }
    }

    this.executor(resolve, reject)
  }

(2)接下来要实现 promise 对象上的 then 方法,then 方法会传入两个函数作为参数,分别作为 promise 对象 resolve 和 reject 的处理函数。
这里要注意三点:

  • then 函数需要返回一个新的 promise 对象
  • 执行 then 函数的时候这个 promise 的状态可能还没有被扭转为 fulfilled 或 rejected
  • 一个 promise 对象可以同时多次调用 then 函数
class MyPromise {
  constructor(executor) {
    this.executor = executor
    this.value = null
    this.status = 'pending'
    this.onFulfilledFunctions = [] // 存放这个 promise 注册的 then 函数中传的第一个函数参数
    this.onRejectedFunctions = [] // 存放这个 promise 注册的 then 函数中传的第二个函数参数
    const resolve = value => {
      if (this.status === 'pending') {
        this.value = value
        this.status = 'fulfilled'
        this.onFulfilledFunctions.forEach(onFulfilled => {
          onFulfilled() // 将 onFulfilledFunctions 中的函数拿出来执行
        })
      }
    }
    const reject = value => {
      if (this.status === 'pending') {
        this.value = value
        this.status = 'rejected'
        this.onRejectedFunctions.forEach(onRejected => {
          onRejected() // 将 onRejectedFunctions 中的函数拿出来执行
        })
      }
    }
    this.executor(resolve, reject)
  }

  then(onFulfilled, onRejected) {
    const self = this
    if (this.status === 'pending') {
      /**
       *  当 promise 的状态仍然处于 ‘pending’ 状态时,需要将注册 onFulfilled、onRejected 方法放到 promise 的 onFulfilledFunctions、onRejectedFunctions 备用
       */
      return new MyPromise((resolve, reject) => {
        this.onFulfilledFunctions.push(() => {
          const thenReturn = onFulfilled(self.value)
          resolve(thenReturn)
        })
        this.onRejectedFunctions.push(() => {
          const thenReturn = onRejected(self.value)
          resolve(thenReturn)
        })
      })
    } else if (this.status === 'fulfilled') {
      return new MyPromise((resolve, reject) => {
        const thenReturn = onFulfilled(self.value)
        resolve(thenReturn)
      })
    } else {
      return new MyPromise((resolve, reject) => {
        const thenReturn = onRejected(self.value)
        resolve(thenReturn)
      })
    }
  }
}

对于以上完成的 MyPromise 进行测试,测试代码如下

const p = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 1000)
})

p.then(res => {
  console.log('first then', res)
  return res + 1
}).then(res => {
  console.log('first then', res)
})

p.then(res => {
  console.log(`second then`, res)
  return res + 1
}).then(res => {
  console.log(`second then`, res)
})

/**
 *  输出结果如下:
 *  first then 1
 *  first then 2
 *  second then 1
 *  second then 2
 */

(3)在 promise 相关的内容中,有一点常常被我们忽略,当 then 函数中返回的是一个 promise 应该如何处理?
考虑如下代码:

// 使用正确的 Promise
new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve()
  }, 1000)
})
  .then(res => {
    console.log('外部 promise')
    return new Promise((resolve, reject) => {
      resolve(`内部 promise`)
    })
  })
  .then(res => {
    console.log(res)
  })

/**
 * 输出结果如下:
 * 外部 promise
 * 内部 promise
 */

通过以上的输出结果不难判断,当 then 函数返回的是一个 promise 时,promise 并不会直接将这个 promise 传递到下一个 then 函数,而是会等待该 promise resolve 后,将其 resolve 的值,传递给下一个 then 函数,找到我们实现的代码的 then 函数部分,做以下修改:

then(onFulfilled, onRejected) {
    const self = this
    if (this.status === 'pending') {
        return new MyPromise((resolve, reject) => {
        this.onFulfilledFunctions.push(() => {
            const thenReturn = onFulfilled(self.value)
            if (thenReturn instanceof MyPromise) {
                // 当返回值为 promise 时,等该内部的 promise 状态扭转时,同步扭转外部的 promise 状态
                thenReturn.then(resolve, reject)
            } else {
                resolve(thenReturn)
            }
        })
        this.onRejectedFunctions.push(() => {
            const thenReturn = onRejected(self.value)
            if (thenReturn instanceof MyPromise) {
                // 当返回值为 promise 时,等该内部的 promise 状态扭转时,同步扭转外部的 promise 状态
                thenReturn.then(resolve, reject)
            } else {
                resolve(thenReturn)
            }
        })
        })
    } else if (this.status === 'fulfilled') {
        return new MyPromise((resolve, reject) => {
            const thenReturn = onFulfilled(self.value)
            if (thenReturn instanceof MyPromise) {
                // 当返回值为 promise 时,等该内部的 promise 状态扭转时,同步扭转外部的 promise 状态
                thenReturn.then(resolve, reject)
            } else {
                resolve(thenReturn)
            }
        })
    } else {
        return new MyPromise((resolve, reject) => {
            const thenReturn = onRejected(self.value)
            if (thenReturn instanceof MyPromise) {
                // 当返回值为 promise 时,等该内部的 promise 状态扭转时,同步扭转外部的 promise 状态
                thenReturn.then(resolve, reject)
            } else {
                resolve(thenReturn)
            }
        })
    }
}

(4) 之前的 promise 实现代码仍然缺少很多细节逻辑,下面会提供一个相对完整的版本,注释部分是增加的代码,并提供了解释。

class MyPromise {
  constructor(executor) {
    this.executor = executor
    this.value = null
    this.status = 'pending'
    this.onFulfilledFunctions = []
    this.onRejectedFunctions = []
    const resolve = value => {
      if (this.status === 'pending') {
        this.value = value
        this.status = 'fulfilled'
        this.onFulfilledFunctions.forEach(onFulfilled => {
          onFulfilled()
        })
      }
    }
    const reject = value => {
      if (this.status === 'pending') {
        this.value = value
        this.status = 'rejected'
        this.onRejectedFunctions.forEach(onRejected => {
          onRejected()
        })
      }
    }
    this.executor(resolve, reject)
  }

  then(onFulfilled, onRejected) {
    const self = this
    if (typeof onFulfilled !== 'function') {
      // 兼容 onFulfilled 未传函数的情况
      onFulfilled = function() {}
    }
    if (typeof onRejected !== 'function') {
      // 兼容 onRejected 未传函数的情况
      onRejected = function() {}
    }
    if (this.status === 'pending') {
      return new MyPromise((resolve, reject) => {
        this.onFulfilledFunctions.push(() => {
          try {
            const thenReturn = onFulfilled(self.value)
            if (thenReturn instanceof MyPromise) {
              thenReturn.then(resolve, reject)
            } else {
              resolve(thenReturn)
            }
          } catch (err) {
            // catch 执行过程的错误
            reject(err)
          }
        })
        this.onRejectedFunctions.push(() => {
          try {
            const thenReturn = onRejected(self.value)
            if (thenReturn instanceof MyPromise) {
              thenReturn.then(resolve, reject)
            } else {
              resolve(thenReturn)
            }
          } catch (err) {
            // catch 执行过程的错误
            reject(err)
          }
        })
      })
    } else if (this.status === 'fulfilled') {
      return new MyPromise((resolve, reject) => {
        try {
          const thenReturn = onFulfilled(self.value)
          if (thenReturn instanceof MyPromise) {
            thenReturn.then(resolve, reject)
          } else {
            resolve(thenReturn)
          }
        } catch (err) {
          // catch 执行过程的错误
          reject(err)
        }
      })
    } else {
      return new MyPromise((resolve, reject) => {
        try {
          const thenReturn = onRejected(self.value)
          if (thenReturn instanceof MyPromise) {
            thenReturn.then(resolve, reject)
          } else {
            resolve(thenReturn)
          }
        } catch (err) {
          // catch 执行过程的错误
          reject(err)
        }
      })
    }
  }
}

(5)至此一个相对完整的 promise 已经实现,但他仍有一些问题,了解宏任务、微任务的同学一定知道,promise 的 then 函数实际上是注册一个微任务,then 函数中的参数函数并不会同步执行。
查看如下代码:

new Promise((resolve,reject)=>{
    console.log(`promise 内部`)
    resolve()
}).then((res)=>{
    console.log(`第一个 then`)
})
console.log(`promise 外部`)

/**
 * 输出结果如下:
 * promise 内部
 * promise 外部
 * 第一个 then
 */

// 但是如果使用我们写的 MyPromise 来执行上面的程序

new MyPromise((resolve,reject)=>{
    console.log(`promise 内部`)
    resolve()
}).then((res)=>{
    console.log(`第一个 then`)
})
console.log(`promise 外部`)
/**
 * 输出结果如下:
 * promise 内部
 * 第一个 then
 * promise 外部
 */

以上的原因是因为的我们的 then 中的 onFulfilled、onRejected 是同步执行的,当执行到 then 函数时上一个 promise 的状态已经扭转为 fulfilled 的话就会立即执行 onFulfilled、onRejected。
要解决这个问题也非常简单,将 onFulfilled、onRejected 的执行放在下一个事件循环中就可以了。

if (this.status === 'fulfilled') {
  return new MyPromise((resolve, reject) => {
    setTimeout(() => {
      try {
        const thenReturn = onFulfilled(self.value)
        if (thenReturn instanceof MyPromise) {
          thenReturn.then(resolve, reject)
        } else {
          resolve(thenReturn)
        }
      } catch (err) {
        // catch 执行过程的错误
        reject(err)
      }
    })
  }, 0)
}

关于宏任务和微任务的解释,我曾在掘金上看到过一篇非常棒的文章,它用银行柜台的例子解释了为什么会同时存在宏任务和微任务两个队列,文章链接贴到文末感兴趣的可以看一下。

5.Promise/A+ 方案解读

我们上面实现的一切逻辑,均是按照 Promise/A+ 规范实现的,Promise/A+ 规范说的大部分内容已经在上面 promise 的实现过程中一一讲解。接下来讲述相当于一个汇总:

  1. promise 有三个状态 pending、fulfilled、rejected,只能由 pending 向 fulfilled 、rejected 两种状态发生改变。
  2. promise 需要提供一个 then 方法,then 方法接收 (onFulfilled,onRejected) 两个函数作为参数。
  3. onFulfilled、onRejected 须在 promise 完成后后(状态扭转)后调用,且只能调用一次。
  4. onFulfilled、onRejected 仅仅作为函数进行调用,不能够将 this 指向调用它的 promise。
  5. onFulfilled、onRejected 必须在执行上下文栈只包含平台代码后才能执行。平台代码指 引擎,环境,Promise 实现代码。(PS:这处规范要求 onFulfilled、onRejected 函数的执行必须在 then 被调用的那个事件循环之后的事件循环。但是规范并没有要求是把它们作为一个微任务或是宏任务去执行,只是各平台的实现均把 Promise 的 onFulfilled、onRejected 放到微任务队列中去执行了)
  6. onFulfilled、onRejected 必须是个函数,否则忽略。
  7. then 方法可以被一个 promise 多次调用。
  8. then 方法需要返回一个 promise。
  9. Promise 的解析过程是一个抽象操作,将 Promise 和一个值作为输入,我们将其表示为 [[Resolve]](promise,x)[[Resolve]](promise,x) 是创建一个 Resolve 方法并传入 promise,x(promise 成功时返回的值) 两个参数,如果 x 是一个 thenable 对象(含有 then 方法),并且假设 x 的行为类似 promise, [[Resolve]](promise,x) 会创造一个采用 x 状态的 promise,否则 [[Resolve]](promise,x) 会用 x 来扭转 promise 的状态。取得输入的不同的 promise 实现方式可以进行交互,只要它们都暴露了 Promise/A+ 兼容方法即可。它也允许 promise 使用合理的 then 方法同化一些不合规范的 promise 实现。

第 9 点只看文档比较晦涩难懂,其实它是针对我们的 then 方法中的这行代码做的规范解释。

return new MyPromise((resolve, reject) => {
  try {
    const thenReturn = onFulfilled(self.value)
    if (thenReturn instanceof MyPromise) {
      // 👈 就是这一行代码
      thenReturn.then(resolve, reject)
    } else {
      resolve(thenReturn)
    }
  } catch (err) {
    reject(err)
  }
})

因为 Promise 并不是 JS 一开始就有的标准,是被很多第三方独立实现的一个方法,所以无法通过 instanceof 来判断返回值是否是一个 promise 对象,所以为了使不同的 promise 可以交互,才有了我上面提到的第 9 条规范。当返回值 thenReturn 是一个 promise 对象时,我们需要等待这个 promise 的状态发生扭转并用它的返回值来 resolve 外层的 promise。

所以最后我们还需要实现 [[Resolve]](promise,x),来满足 promise 规范,规范如下所示。

promiseRule

/**
 * resolvePromise 函数即为根据 x 的值来决定 promise2 的状态的函数
 * @param {Promise} promise2  then 函数需要返回的 promise 对象
 * @param {any} x onResolve || onReject 执行后得到的返回值
 * @param {Function} resolve  MyPromise 中的 resolve 方法
 * @param {Function} reject  MyPromise 中的 reject 方法
 */
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    // 2.3.1 promise2 和 x 指向同一个对象
    reject(new TypeError())
    return
  }

  if (x instanceof MyPromise) {
    // 2.3.2 x 是一个 MyPromise 的实例,采用他的状态
    if (x.status === 'pending') {
      x.then(
        value => {
          resolvePromise(promise2, value, resolve, reject)
        },
        err => {
          reject(err)
        }
      )
    } else {
      x.then(resolve, reject)
    }
    return
  }

  if (x && (typeof x === 'function' || typeof x === 'object')) {
    // 2.3.3 x 是一个对象或函数
    try {
      const then = x.then // 2.3.3.1 声明 变量 then = x.then
      let promiseStatusConfirmed = false // promise 的状态确定
      if (typeof then === 'function') {
        // 2.3.3.3 then 是一个方法,把 x 绑定到 then 函数中的 this 上并调用
        then.call(
          x,
          value => {
            // 2.3.3.3.1 then 函数返回了值 value,则使用 [[Resolve]](promise, value),用于监测 value 是不是也是一个 thenable 的对象
            if (promiseStatusConfirmed) return // 2.3.3.3.3 即这三处谁选执行就以谁的结果为准
            promiseStatusConfirmed = true
            resolvePromise(promise2, value, resolve, reject)
            return
          },
          err => {
            // 2.3.3.3.2  then 函数抛错 err ,用 err reject 当前的 promise
            if (promiseStatusConfirmed) return // 2.3.3.3.3 即这三处谁选执行就以谁的结果为准
            promiseStatusConfirmed = true
            reject(err)
            return
          }
        )
      } else {
        // 2.3.3.4  then 不是一个方法,则用 x 扭转 promise 状态 为 fulfilled
        resolve(x)
      }
    } catch (e) {
      // 2.3.3.2 在取得 x.then 的结果时抛出错误 e 的话,使用 e reject 当前的 promise
      if (promiseStatusConfirmed) return // 2.3.3.3.3 即这三处谁选执行就以谁的结果为准
      promiseStatusConfirmed = true
      reject(e)
      return
    }
  } else {
    resolve(x) // 2.3.4 如果 x 不是 object || function,用 x 扭转 promise 状态 为 fulfilled
  }
}

然后我们就可以用 resolcePromise 方法替换之前的这部分代码

return new MyPromise((resolve, reject) => {
  try {
    const thenReturn = onFulfilled(self.value)
    if (thenReturn instanceof MyPromise) {
      thenReturn.then(resolve, reject)
    } else {
      resolve(thenReturn)
    }
  } catch (err) {
    reject(err)
  }
})

// 变成下面这样 👇 

return new MyPromise((resolve, reject) => {
  try {
    const thenReturn = onFulfilled(self.value)
    resolvePromise(resolve,reject)
  } catch (err) {
    reject(err)
  }
})

本篇文章不在于实现一个完整的 promise,但是通过对 promise 的尝试实现,已经对 promise 有了更加深入的了解,这样的实现过程可以帮助开发者在开发过程中更好的使用 promise 。如果希望能够实现一份完整的 promise,可以再仔细阅读一下下面的参考内容,并用 promise 的测试用例来检验自己的实现结果。

相关参考
Promise/A+ 规范
从零一步一步实现一个完整版的 Promise
微任务、宏任务与Event-Loop

查看原文

赞 35 收藏 23 评论 0

2FPS 赞了文章 · 2019-12-18

2020年大前端面试题库 - 备战明年金三银四

推荐下我自己的小册 React SSR 服务端渲染原理解析与实践

全网最完整的 React SSR 同构技术原理解析与实践,从零开始手把手带你打造自己的同构应用开发骨架,帮助大家彻底深入理解服务端渲染及底层实现原理,学完本课程,你也可以打造自己的同构框架。

写在前面

现在面试文章已很多,更不觉得新鲜,本文可能有点不同,正因为“多”也就才有了本文的输出。

相信很多前端小伙伴也包括我在内,面试前都要做一些准备,而做做面试题是最平常不过的事儿了,然而每次面试前都要现找面试题,而且答案也不是现成的,其实这样重复的事情在不知不觉中浪费你很多时间。

也就因为发现了这个问题,才有了把面试题进行整理的想法,希望能对一些人有所帮助,节省一点时间吧。

题目整理

这1个多月来一直在整理前端领域相关的面试题,阅读查看了将近100多篇的面试文章,文章大多来自技术社区,如掘金、思否等,由于很多文章的题目重复性较大,所以整理起来比较费事。

到目前为止已经收集了将近500道,包含问答题、代码题、高频基础、大厂题目等。

下面是部分问题:

第1题 谈谈变量提升?
第2题 说说bind、call、apply的 区别?
第3题 如何实现一个 bind 函数?
第4题 请实现一个 call 函数
第5题 如何实现一个 apply 函数?
第6题 简单说下原型链?
第7题 怎么判断对象类型?
第8题 说说箭头函数的特点
第9题 如何确定This指向
第10题 async、await 的优缺点
第11题 generator 原理
第12题 对Promise的理解
第13题 == 和 ===区别,什么情况用 ==
第14题 垃圾回收 新生代算法,老生代算法
第15题 说说你对闭包的理解
第16题 基本数据类型和引⽤类型在存储上的差别
第17题 浏览器 Eventloop 和 Node 中的有什么区别
第18题 怎样理解setTimeout 执行误差
第19题 说说函数节流和防抖
第20题 数组降维
第21题 请实现一个深拷贝
第22题 typeof 于 instanceof 区别
第23题 cookie和localSrorage、session、indexDB 的区别
第24题 怎么判断页面是否加载完成?
第25题 说说 jsonp 原理
第26题 说说你对Service worker的理解
第27题 说说浏览器缓存机制
第28题 怎样选择合适的缓存策略
第29题 说说重绘(Repaint)和回流(Reflow)
第30题 如何优化图片
第31题 页面首屏渲染性能优化方案有哪些
第32题 浏览器性能问题-使用 Webpack 优化项目
第33题 Babel 原理
第34题 介绍下React 生命周期
第35题 react setState 机制
第36题 Vue的 nextTick 原理
第37题 Vue 生命周期
第38题 Vue 双向绑定
第39题 v-model原理
第40题 watch 和 computed 的区别和运用的场景
第41题 Vue 的父子通信
第42题 简述路由原理
第43题 MVVM-脏数据检测
第44题 MVVM-数据劫持
第45题 React V16 生命周期函数用法
第46题 Vue 和 React 区别
第47题 介绍下虚拟 DOM,对虚拟 DOM 的理解
第48题 路由鉴权
第49题 TCP 3次握手
第50题 TCP 拥塞控制
第51题 慢开始算法
第52题 拥塞避免算法
第53题 tcp 快速重传
第54题 TCP New Ren 改进后的快恢复
第55题 HTTPS 握手
第56题 从输入 URL 到页面加载全过程
第57题 HTTP 常用状态码 301 302 304 403
第58题 常见排序-冒泡排序
第59题 常见排序-插入排序
第60题 常见排序-选择排序
第61题 常见排序-归并排序
第62题 常见排序-快排
第63题 常见排序-堆排序
第64题 常见排序-系统自带排序实现
第65题 介绍下设计模式-工厂模式
第66题 介绍下设计模式-单例模式
第67题 介绍下设计模式-适配器模式
第68题 介绍下设计模式-装饰模式
第69题 介绍下设计模式-代理模式
第70题 介绍下设计模式-发布-订阅模式
第71题 Vue 响应式原理
第72题 实现一个new操作符
第73题 实现一个JSON.stringify
第74题 实现一个JSON.parse
第75题 手写一个继承
第76题 实现一个JS函数柯里化
第77题 请手写一个Promise(中高级必考)
第78题 手写防抖(Debouncing)和节流(Throttling)
第79题 实现一个instanceOf
第80题 实现一个私有变量
第81题 使用setTimeout代替setInterval进行间歇调用
第82题 数组中的forEach和map的区别
第83题 for in和for of的区别
第84题 写一个发布订阅 EventEmitter方法
第85题 let、var、const区别
第86题 typeof和instanceof 区别
第87题 常见的继承的几种方法
第88题 常见的浏览器内核有哪些?
第89题 浏览器的主要组成部分是什么?
第90题 浏览器是如何渲染UI的?
第91题 浏览器如何解析css选择器?
第92题 DOM Tree是如何构建的?
第93题 重绘与重排的区别?
第94题 如何触发重排和重绘?
第95题 如何避免重绘或者重排?
第96题 前端如何实现即时通讯?
第97题 什么是浏览器同源策略?
第98题 怎样解决跨域问题?
第99题 时间格式化
第100题 说说对html 语义化的理解
第101题 说说常用的 meta 标签
第102题 说说两种盒模型以及区别
第103题 css reset 和 normalize.css 有什么区别
第104题 怎样让元素水平垂直居中
第105题 说说选择器的权重计算方式
第106题 清除浮动的方法
第107题 说说你对 BFC 的理解
第108题 import 和 link 区别
第109题 说下 [1, 2, 3].map(parseInt) 结果
第110题 介绍下浏览器事件委托
第111题 10w 条记录的数组,一次性渲染到页面上,如何处理可以不冻结UI?
第112题 如何实现一个左右固定,中间自适应的三栏布局
第113题 如何实现一个自适应的正方形
第114题 如何用css实现一个三角形
第115题 介绍下 positon 属性
第116题 说说渐进增强和优雅降级
第117题 defer和async区别
第118题 实现sleep函数
第119题 实现 lazyMan
第120题 获取元素的最终background-color
....

解析呢?请继续往下看。

利用碎片时间系统刷题

为了方便管理和维护这些题目,同时为前端小伙伴儿提供更好看题体验,我简单做了一个题库网站-大前端面试题库,宗旨是充分利用大家的碎片时间,在上下班,地铁公交上刷刷题,日积月累,尽量避免临时抱佛脚的局面。

另外对面试题做了一个简单的类型划分,进行分类划分的重要性不言而喻,碎片化的信息会极大的消耗我们精力,同时也不利于我们记忆,人脑自然的更喜欢结构化的,有规则的信息,通过系统的只处理同一种类型的问题来降低大脑的损耗,加强我们对知识的理解和记忆。

分类大概为js、经典高频、编程题、大厂题、html、css、布局、浏览器、性能、前端框架、react、vue、web 安全、数据结构和算法、http、tcp、node、设计模式等。

这个网站是用的我自己的 react ssr 开发骨架做的,目前支持 pc 和移动端浏览。

看下页面效果

解析/答案

由于个人精力有限,一时无法写完所有的问题解析。目前仍在继续完善中,这个过程也是对自己一种提升和沉淀,我觉得这个价值很大。

不过为了方便大家校对答案,大部分问题的解析我单独做了链接跳转,指向了原文,可直接跳转到来源文章查看解析。

一点心得

这个题库的价值不只是用来刷题,他的价值在于问题本身,问题本身比答案更重要。

通过问题来验证自己的知识技能,核对自己对知识的掌握程度,这完全可以当做一种学习方法来执行。

当我们遇到的问题越多,理解和解决的问题越多,相对的我们的能力就会越强。
这500+看上去确实挺多的了,覆盖面也挺广了,但是前端技术日新月异,新技术新思路层出不穷,相关的问题也会不断的更新迭代,所以这个题库还要继续更新,后面继续把更有针对性的问题收集进来。

以上一些观点有些属于本人自嗨,现在回归初心,收集题库并创建题库站的最终目的是希望帮一些人提升一点效率,节约一点时间,利用碎片时间,上下班地铁上刷个题,如果还能帮你提升技能,提升搞定面试的几率,那就最好不过了。

题库入口1

为了方便大家找到入口不迷路,题库入口绑定到了公众号的独立菜单。

我的公众号是-《前端技术江湖》,主要是个人的原创和一些工作心得的输出,可以及时的获得题库的更新通知,另外还有很多优质文章和前端学习资料,希望大家多多关注。

题库入口2

网站入口 - 大前端面试题库- http://bigerfe.com/,内有福利哦。

最后一件事

我正在打造一个纯技术交流群,面向初中级前端开发者,以学习、交流、思考、提升能力为目标,因为一个人学不如大家一起学,有了更多的交流才会进步的更快。

我理想的模式是,每期让一个人深入学习一个技术,然后自己再转述给大家听,类似一个分享课堂,这样可以成倍的提升学习效率。

或者可以按照题库的顺序依次进行,每人每天对对一个问题进行思考和总结性的输出,锻炼技术的同时提升表达能力。

在这个群里不用担心自己的能力不足,不用担心问题是否太小白而不敢说,大胆的说出问题, 让更多的人一起来分析,说错了也没关系。

有想加入请的关注公众号《前端技术江湖》,回复‘进群’,我拉你进群,另外还有各种学习资料和学习视频


希望本文可以给你带了一些帮助和便利,文中如有错误,欢迎在评论区指。
如果这篇文章帮助到了你,欢迎点赞和关注。

查看原文

赞 72 收藏 50 评论 6

2FPS 赞了文章 · 2019-12-10

通过Node.js的Cluster模块源码,深入PM2原理

Node.js无疑是走向大前端、全栈工程师技术栈最快的捷径(但是一定要会一门其他后台语言,推荐Golang),虽然Node.js做很多事情都做不好,但是在某些方面还是有它的优势。

众所周知,Node.js中的JavaScript代码执行在单线程中,非常脆弱,一旦出现了未捕获的异常,那么整个应用就会崩溃。

这在许多场景下,尤其是web应用中,是无法忍受的。通常的解决方案,便是使用Node.js中自带的cluster模块,以master-worker模式启动多个应用实例。然而大家在享受cluster模块带来的福祉的同时,不少人也开始好奇

1.为什么我的应用代码中明明有app.listen(port);,但cluter模块在多次fork这份代码时,却没有报端口已被占用?

2.Master是如何将接收的请求传递至worker中进行处理然后响应的?

带着这些疑问我们开始往下看

TIPS:

本文编写于2019年12月8日,是最新版本的Node.js源码

Cluster源码解析:

  • 入口 :
const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master';

module.exports = require(`internal/cluster/${childOrMaster}`);
  • 分析

会根据一个当前的Node_UNIQUE_ID(后面会讲)是否在环境变量中判断是子进程还是主进程,然后引用不同的js代码

NODE_UNIQUE_ID是一个唯一标示,Node.js的Cluster多进程模式,采用默认的调度算法是round-robin,其实就是轮询.官方解释是实践效率非常高,稳定

之前的问题一: 为什么我的应用代码中明明有app.listen(port);,但cluter模块在多次fork这份代码时,却没有报端口已被占用?

我在Node.js的官网找到了答案:

原来所有的net.Socket都被设置了SO_REUSEADDR

这个SO_REUSEADDR到底是什么呢?

为什么需要 SO_REUSEADDR 参数?

服务端主动断开连接以后,需要等 2 个 MSL 以后才最终释放这个连接,重启以后要绑定同一个端口,默认情况下,操作系统的实现都会阻止新的监听套接字绑定到这个端口上。

我们都知道 TCP 连接由四元组唯一确定。形式如下

{local-ip-address:local-port , foreign-ip-address:foreign-port}

一个典型的例子如下图

TCP 要求这样的四元组必须是唯一的,但大多数操作系统的实现要求更加严格,只要还有连接在使用这个本地端口,则本地端口不能被重用(bind 调用失败)

启用 SO_REUSEADDR 套接字选项可以解除这个限制,默认情况下这个值都为 0,表示关闭。在 Java 中,reuseAddress 不同的 JVM 有不同的实现,在我本机上,这个值默认为 1 允许端口重用。但是为了保险起见,写 TCP、HTTP 服务一定要主动设置这个参数为 1。

目前常见的网络编程模型就是多进程或多线程,根据accpet的位置,分为如下场景

2种场景

(1) 单进程或线程创建socket,并进行listenaccept,接收到连接后创建进程和线程处理连接

(2) 单进程或线程创建socket,并进行listen,预先创建好多个工作进程或线程accept()在同一个服务器套接字

这两种模型解充分发挥了多核CPU的优势,虽然可以做到线程和CPU核绑定,但都会存在:

1.单一listener工作进程或线程在高速的连接接入处理时会成为瓶颈

2.多个线程之间竞争获取服务套接字

3.缓存行跳跃

4.很难做到CPU之间的负载均衡

5.随着核数的扩展,性能并没有随着提升

6.SO_REUSEPORT解决了什么问题

7.SO_REUSEPORT支持多个进程或者线程绑定到同一端口,提高服务器程序的性能

解决的问题:

1.允许多个套接字 bind()/listen() 同一个TCP/UDP端口

2.每一个线程拥有自己的服务器套接字

3.在服务器套接字上没有了锁的竞争

4.内核层面实现负载均衡

5.安全层面,监听同一个端口的套接字只能位于同一个用户下面

其核心的实现主要有三点:

1.扩展 socket option,增加 SO_REUSEPORT 选项,用来设置 reuseport

2.修改 bind 系统调用实现,以便支持可以绑定到相同的 IP 和端口

3.修改处理新建连接的实现,查找 listener 的时候,能够支持在监听相同 IP 4.和端口的多个 sock 之间均衡选择。

5.有了SO_RESUEPORT后,每个进程可以自己创建socket、bind、listen、accept相同的地址和端口,各自是独立平等的

让多进程监听同一个端口,各个进程中accept socket fd不一样,有新连接建立时,内核只会唤醒一个进程来accept,并且保证唤醒的均衡性。

总结:原来端口被复用是因为设置了SO_REUSEADDR,当然不止这一点,下面会继续描述

回到源码第一行

NODE_UNIQUE_ID是什么?

下面给出介绍:


function createWorkerProcess(id, env) {
  // ...
  workerEnv.NODE_UNIQUE_ID = '' + id;
​
​
  // ...
  return fork(cluster.settings.exec, cluster.settings.args, {
    env: workerEnv,
    silent: cluster.settings.silent,
    execArgv: execArgv,
    gid: cluster.settings.gid,
    uid: cluster.settings.uid
  });
}
​

原来,创建子进程的时候,给了每个进程一个唯一的自增标示ID

随后Node.js在初始化时,会根据该环境变量,来判断该进程是否为cluster模块fork出的工作进程,若是,则执行workerInit()函数来初始化环境,否则执行masterInit()函数

就是这行入口的代码~


module.exports = require(`internal/cluster/${childOrMaster}`);

接下来我们需要看一下net模块的listen函数源码:


// lib/net.js
// ...
​
function listen(self, address, port, addressType, backlog, fd, exclusive) {
  exclusive = !!exclusive;
​
  if (!cluster) cluster = require('cluster');
​
  if (cluster.isMaster || exclusive) {
    self._listen2(address, port, addressType, backlog, fd);
    return;
  }
​
  cluster._getServer(self, {
    address: address,
    port: port,
    addressType: addressType,
    fd: fd,
    flags: 0
  }, cb);
​
  function cb(err, handle) {
    // ...
​
    self._handle = handle;
    self._listen2(address, port, addressType, backlog, fd);
  }
}

仔细一看,原来listen函数会根据是不是主进程做不同的操作!

上面有提到SO_REUSEADDR选项,在主进程调用的_listen2中就有设置。

子进程初始化的每个workerinit函数中,也有cluster._getServer这个方法,

你可能已经猜到,问题一的答案,就在这个cluster._getServer函数的代码中。它主要干了两件事:

  • master进程注册该worker,若master进程是第一次接收到监听此端口/描述符下的worker,则起一个内部TCP服务器,来承担监听该端口/描述符的职责,随后在master中记录下该worker。
  • Hack掉worker进程中的net.Server实例的listen方法里监听端口/描述符的部分,使其不再承担该职责。

对于第一件事,由于master在接收,传递请求给worker时,会符合一定的负载均衡规则(在非Windows平台下默认为轮询),这些逻辑被封装在RoundRobinHandle类中。故,初始化内部TCP服务器等操作也在此处:

// lib/cluster.js
// ...
​
function RoundRobinHandle(key, address, port, addressType, backlog, fd) {
  // ...
  this.handles = [];
  this.handle = null;
  this.server = net.createServer(assert.fail);
​
  if (fd >= 0)
    this.server.listen({ fd: fd });
  else if (port >= 0)
    this.server.listen(port, address);
  else
    this.server.listen(address);  // UNIX socket path.
  /// ...
}

在子进程中:



function listen(backlog) {
    return 0;
  }
​
  function close() {
    // ...
  }
  function ref() {}
  function unref() {}
​
  var handle = {
    close: close,
    listen: listen,
    ref: ref,
    unref: unref,
  }

由于net.Server实例的listen方法,最终会调用自身_handle属性下listen方法来完成监听动作,故在代码中修改之:此时的listen方法已经被hack ,每次调用只能发挥return 0 ,并不会监听端口

// lib/net.js
// ...
function listen(self, address, port, addressType, backlog, fd, exclusive) {
  // ...
​
  if (cluster.isMaster || exclusive) {
    self._listen2(address, port, addressType, backlog, fd);
    return; // 仅在worker环境下改变
  }
​
  cluster._getServer(self, {
    address: address,
    port: port,
    addressType: addressType,
    fd: fd,
    flags: 0
  }, cb);
​
  function cb(err, handle) {
    // ...
    self._handle = handle;
    // ...
  }
}

这里可以看到,传入的回调函数中的handle,已经把listen方法重新定义,返回0,那么等子进程调用listen方法时候,也是返回0,并不会去监听端口,至此,焕然大悟,原来是这样,真正监听端口的始终只有主进程!

上面通过将近3000字讲解,把端口复用这个问题讲清楚了,下面把负载均衡这块也讲清楚。然后再讲PM2的原理实现,其实不过是对cluster模式进行了封装,多了很多功能而已~


首先画了一个流程图

核心实现源码:

function RoundRobinHandle(key, address, port, addressType, backlog, fd) {
  // ...
  this.server = net.createServer(assert.fail);
  // ...
​
  var self = this;
  this.server.once('listening', function() {
    // ...
    self.handle.onconnection = self.distribute.bind(self);
  });
}
​
RoundRobinHandle.prototype.distribute = function(err, handle) {
  this.handles.push(handle);
  var worker = this.free.shift();
  if (worker) this.handoff(worker);
};
​
RoundRobinHandle.prototype.handoff = function(worker) {
  // ...
  var message = { act: 'newconn', key: this.key };
  var self = this;
  sendHelper(worker.process, message, handle, function(reply) {
    // ...
  });

解析

定义好handle对象中的onconnection方法

触发事件时,取出一个子进程通知,传入句柄

子进程接受到消息和句柄后,做相应的业务处理:


// lib/cluster.js
// ...
​
// 该方法会在Node.js初始化时由 src/node.js 调用
cluster._setupWorker = function() {
  // ...
  process.on('internalMessage', internal(worker, onmessage));
​
  // ...
  function onmessage(message, handle) {
    if (message.act === 'newconn')
      onconnection(message, handle);
    // ...
  }
};
​
function onconnection(message, handle) {
  // ...
  var accepted = server !== undefined;
  // ...
  if (accepted) server.onconnection(0, handle);
}

总结下来,负载均衡大概流程:

1.所有请求先同一经过内部TCP服务器,真正监听端口的只有主进程。

2.在内部TCP服务器的请求处理逻辑中,有负载均衡地挑选出一个worker进程,将其发送一个newconn内部消息,随消息发送客户端句柄。

3.Worker进程接收到此内部消息,根据客户端句柄创建net.Socket实例,执行具体业务逻辑,返回。

至此,Cluster多进程模式,负载均衡讲解完毕,下面讲PM2的实现原理,它是基于Cluster模式的封装


PM2的使用:

npm i pm2 -g 
pm2 start app.js 
pm2 ls

这样就可以启动你的Node.js服务,并且根据你的电脑CPU个数去启动相应的进程数,监听到错误事件,自带重启子进程,即使更新了代码,需要热更新,也会逐个替换,号称永动机。

它的功能:

1.内建负载均衡(使用Node cluster 集群模块)

2.后台运行

3.0秒停机重载,我理解大概意思是维护升级的时候不需要停机.

4.具有Ubuntu和CentOS 的启动脚本

5.停止不稳定的进程(避免无限循环)

6.控制台检测

7.提供 HTTP API

8.远程控制和实时的接口API ( Nodejs 模块,允许和PM2进程管理器交互 )


先来一张PM2的架构图:

pm2包括 Satan进程、God Deamon守护进程、进程间的远程调用rpc、cluster等几个概念

如果不知道点西方文化,还真搞不清他的文件名为啥是 SatanGod

撒旦(Satan),主要指《圣经》中的堕天使(也称堕天使撒旦),被看作与上帝的力量相对的邪恶、黑暗之源,是God的对立面。

1.Satan.js提供了程序的退出、杀死等方法,因此它是魔鬼;God.js 负责维护进程的正常运行,当有异常退出时能保证重启,所以它是上帝。作者这么命名,我只能说一句:oh my god。
God进程启动后一直运行,它相当于cluster中的Master进程,守护者worker进程的正常运行。

2.rpc(Remote Procedure Call Protocol)是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。同一机器不同进程间的方法调用也属于rpc的作用范畴。

3.代码中采用了axon-rpc 和 axon 两个库,基本原理是提供服务的server绑定到一个域名和端口下,调用服务的client连接端口实现rpc连接。后续新版本采用了pm2-axon-rpc 和 pm2-axon两个库,绑定的方法也由端口变成.sock文件,因为采用port可能会和现有进程的端口产生冲突。

执行流程

程序的执行流程图如下:

每次命令行的输入都会执行一次satan程序。如果God进程不在运行,首先需要启动God进程。然后根据指令,satan通过rpc调用God中对应的方法执行相应的逻辑。

pm2 start app.js -i 4为例,God在初次执行时会配置cluster,同时监听cluster中的事件:


// 配置cluster
cluster.setupMaster({
  exec : path.resolve(path.dirname(module.filename), 'ProcessContainer.js')
});
​
// 监听cluster事件
(function initEngine() {
  cluster.on('online', function(clu) {
    // worker进程在执行
    God.clusters_db[clu.pm_id].status = 'online';
  });
​
  // 命令行中 kill pid 会触发exit事件,process.kill不会触发exit
  cluster.on('exit', function(clu, code, signal) {
    // 重启进程 如果重启次数过于频繁直接标注为stopped
    God.clusters_db[clu.pm_id].status = 'starting';
​
    // 逻辑
    ...
  });
})();

在God启动后, 会建立Satan和God的rpc链接,然后调用prepare方法。prepare方法会调用cluster.fork,完成集群的启动

God.prepare = function(opts, cb) {
  ...
  return execute(opts, cb);
};
function execute(env, cb) {
  ...
  var clu = cluster.fork(env);
  ...
  God.clusters_db[id] = clu;
​
  clu.once('online', function() {
    God.clusters_db[id].status = 'online';
    if (cb) return cb(null, clu);
    return true;
  });
​
  return clu;
}

PM2的功能目前已经特别多了,源码阅读非常耗时,但是可以猜测到一些功能的实现:

例如

如何检测子进程是否处于正常活跃状态?

采用心跳检测

每隔数秒向子进程发送心跳包,子进程如果不回复,那么调用kill杀死这个进程
然后再重新cluster.fork()一个新的进程

子进程发出异常报错,如何保证一直有一定数量子进程?


子进程可以监听到错误事件,这时候可以发送消息给主进程,请求杀死自己
并且主进程此时重新调用cluster.fork一个新的子进程

目前不少Node.js的服务,依赖Nginx+pm2+docker来实现自动化+监控部署,

pm2本身也是有监听系统的,分免费版和收费版~

具体可以看官网,以及搜索一些操作手册等进行监控操作,配置起来比较简单,

这里就不做概述了。

https://pm2.keymetrics.io/

如果感觉写得不错,麻烦帮忙点个赞然后分享给你身边多人,原创不易,需要支持~!

欢迎关注微信公众号:前端巅峰

尽量都是原创内容,回复加群就可以加入小姐姐众多的前端交流群~

查看原文

赞 46 收藏 27 评论 3

2FPS 回答了问题 · 2019-10-17

解决Mac VS Code 命令行输入问题

试试切换到英文后,长按cap键

关注 3 回答 3

2FPS 赞了文章 · 2019-08-10

前端硬核面试专题之 CSS 55 问

clipboard.png

前言

本文讲解 55 道前端面试的 CSS 的内容。

复习前端面试的知识,是为了巩固前端的基础知识,最重要的还是平时的积累!

注意:文章的题与题之间用下划线分隔开,答案仅供参考。

笔者技术博客首发地址 GitHub,欢迎关注。

文章原文地址:前端硬核面试专题之 CSS 55 问

前端硬核面试专题的完整版在此:前端硬核面试专题,包含:HTML + CSS + JS + ES6 + Webpack + Vue + React + Node + HTTPS + 数据结构与算法 + Git 。

CSS

盒子模型的理解 ?

  • 标准模式和混杂模式(IE)。
  • 在标准模式下浏览器按照规范呈现页面;
  • 在混杂模式下,页面以一种比较宽松的向后兼容的方式显示。
  • 混杂模式通常模拟老式浏览器的行为以防止老站点无法工作。

clipboard.png

clipboard.png

CSS 盒子模型具有内容 (content)、填充 (padding)、边框 (border)、边界 (margin)这些属性。

我们所说的 width,height 指的是内容 (content) 的宽高。

一个盒子模型的中:

  • 宽度 = width+ pdding(宽) + border(宽)。
  • 高度 = height + padding(高) + border(高)。

如何在页面上实现一个圆形的可点击区域 ?

  • 1、map+area 或者 svg
  • 2、border-radius
  • 3、纯 js 实现,需要求一个点在不在圆上简单算法、获取鼠标坐标等等

实现不使用 border 画出 1px 高的线,在不同浏览器的标准模式与怪异模式下都能保持一致的效果。

<div style="height:1px;overflow:hidden;background:red"></div>

CSS 中哪些属性可以同父元素继承 ?

继承:(X)HTML 元素可以从其父元素那里继承部分 CSS 属性,即使当前元素并没有定义该属性,比如: color,font-size。


box-sizing 常用的属性有哪些 ?分别有什么作用 ?

常用的属性:content-box、 border-box 、inherit

作用

  • content-box(默认):宽度和高度分别应用到元素的内容框。在宽度和高度之外绘制元素的内边距和边框(元素默认效果)。
  • border-box:元素指定的任何内边距和边框都将在已设定的宽度和高度内进行绘制。通过从已设定的宽度和高度分别减去边框和内边距才能得到内容的宽度和高度。

页面导入样式时,使用 link 和 @import 有什么区别 ?

  • link 属于 XHTML 标签,除了加载 CSS 外,还能用于定义 RSS(是一种描述和同步网站内容的格式,是使用最广泛的 XML 应用), 定义 rel 连接属性等作用;
  • 而 @import 是 CSS 提供的,只能用于加载 CSS;
  • 页面被加载的时,link 会同时被加载,而 @import 引用的 CSS 会等到页面被加载完再加载;
  • import 是 CSS2.1 提出的,只在 IE5 以上才能被识别,而 link 是 XHTML 标签,无兼容问题。
  • 总之,link 要优于 @import。

常见兼容性问题?

  • 浏览器默认的 margin 和 padding 不同。解决方案是加一个全局的 *{margin: 0; padding: 0;} 来统一。
  • IE下 event 对象有 event.x,event.y 属性,而 Firefox 下没有。Firefox 下有 event.pageX,event.PageY 属性,而 IE 下没有。

解决办法:var mx = event.x ? event.x : event.pageX;

  • Chrome 中文界面下默认会将小于 12px 的文本强制按照 12px 显示, 可通过加入 CSS 属性 -webkit-text-size-adjust: none; 解决.
  • 超链接访问过后 hover 样式就不出现了,被点击访问过的超链接样式不在具有 hover 和 active 了,解决方法是改变 CSS 属性的排列顺序:

L-V-H-A : a:link {} a:visited {} a:hover {} a:active {}


清除浮动,什么时候需要清除浮动,清除浮动都有哪些方法 ?

一个块级元素如果没有设置 height,那么其高度就是由里面的子元素撑开,如果子元素使用浮动,脱离了标准的文档流,那么父元素的高度会将其忽略,如果不清除浮动,父元素会出现高度不够,那样如果设置 border 或者 background 都得不到正确的解析。

正是因为浮动的这种特性,导致本属于普通流中的元素浮动之后,包含框内部由于不存在其他普通流元素了,也就表现出高度为 0(高度塌陷)。在实际布局中,往往这并不是我们所希望的,所以需要闭合浮动元素,使其包含框表现出正常的高度。

清除浮动的方式

  • 父级 div 定义 height,原理:父级 div 手动定义 height,就解决了父级 div 无法自动获取到高度的问题。 
  • 结尾处加空 div 标签 clear: both,原理:添加一个空 div,利用 css 提高的 clear: both 清除浮动,让父级 div 能自动获取到高度。
  • 父级 div 定义 overflow: hidden,  原理:必须定义 width 或 zoom: 1,同时不能定义 height,使用 overflow: hidden 时,浏览器会自动检查浮动区域的高度 
  • 父级 div 也一起浮动 。
  • 父级 div 定义 display: table 。
  • 父级 div 定义 伪类 :after 和 zoom 。
  • 结尾处加 br 标签 clear: both, 原理:父级 div 定义 zoom: 1 来解决 IE 浮动问题,结尾处加 br 标签 clear: both。

总结:比较好的是倒数第 2 种方式,简洁方便。


如何保持浮层水平垂直居中 ?

一、水平居中 

(1)行内元素解决方案

只需要把行内元素包裹在一个属性 display 为 block 的父层元素中,并且把父层元素添加如下属性即可。

.parent {
    text-align: center;
}

(2)块状元素解决方案
 

.item {
    /* 这里可以设置顶端外边距 */
    margin: 10px auto;
}

(3)多个块状元素解决方案将元素的 display 属性设置为 inline-block,并且把父元素的 text-align 属性设置为 center 即可:

.parent {
    text-align:center;
}

(4)多个块状元素解决方案

使用 flexbox 布局,只需要把待处理的块状元素的父元素添加属性 display: flex 及 justify-content: center 即可。

.parent {
    display: flex;
    justify-content: center;
}

二、垂直居中

(1)单行的行内元素解决方案

.parent {
    background: #222;
    height: 200px;
}

/* 以下代码中,将 a 元素的 height 和 line-height 设置的和父元素一样高度即可实现垂直居中 */
a {
    height: 200px;
    line-height:200px; 
    color: #FFF;
}

(2)多行的行内元素解决方案组合

使用 display: table-cell 和 vertical-align: middle 属性来定义需要居中的元素的父容器元素生成效果,如下:

.parent {
    background: #222;
    width: 300px;
    height: 300px;
    /* 以下属性垂直居中 */
    display: table-cell;
    vertical-align: middle;
}

(3)已知高度的块状元素解决方案

.item{
    position: absolute;
    top: 50%;
    margin-top: -50px;  /* margin-top值为自身高度的一半 */
    padding:0;
}

三、水平垂直居中

(1)已知高度和宽度的元素解决方案 1

这是一种不常见的居中方法,可自适应,比方案 2 更智能,如下:

.item{
    position: absolute;
    margin:auto;
    left:0;
    top:0;
    right:0;
    bottom:0;
}

(2)已知高度和宽度的元素解决方案 2

.item{
    position: absolute;
    top: 50%;
    left: 50%;
    margin-top: -75px;  /* 设置margin-left / margin-top 为自身高度的一半 */
    margin-left: -75px;
}

(3)未知高度和宽度元素解决方案

.item{
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);  /* 使用 css3 的 transform 来实现 */
}

(4)使用 flex 布局实现

.parent{
    display: flex;
    justify-content: center;
    align-items: center;
    /* 注意这里需要设置高度来查看垂直居中效果 */
    background: #AAA;
    height: 300px;
}

position 、float 和 display 的取值和各自的意思和用法

position

  • position 属性取值:static(默认)、relative、absolute、fixed、inherit、sticky。
  • postision:static;始终处于文档流给予的位置。看起来好像没有用,但它可以快速取消定位,让 top,right,bottom,left 的值失效。在切换的时候可以尝试这个方法。
  • 除了 static 值,在其他三个值的设置下,z-index 才会起作用。确切地说 z-index 只在定位元素上有效。
  • position:relative 和 absolute 都可以用于定位,区别在于前者的 div 还属于正常的文档流,后者已经是脱离了正常文档流,不占据空间位置,不会将父类撑开。

定位原点 relative 是相对于它在正常流中的默认位置偏移,它原本占据的空间任然保留;absolute 相对于第一个 position 属性值不为 static 的父类。所以设置了 position:absolute,其父类的该属性值要注意,而且 overflow:hidden 也不能乱设置,因为不属于正常文档流,不会占据父类的高度,也就不会有滚动条。

  • fixed 旧版本 IE 不支持,却是很有用,定位原点相对于浏览器窗口,而且不能变。

常用于 header,footer 或者一些固定的悬浮 div,随滚动条滚动又稳定又流畅,比 JS 好多了。fixed 可以有很多创造性的布局和作用,兼容性是问题。

  • position:inherit。

规定从父类继承 position 属性的值,所以这个属性也是有继承性的,但需要注意的是 IE8 以及往前的版本都不支持 inherit 属性。

  • sticky :设置了sticky 的元素,在屏幕范围(viewport)时该元素的位置并不受到定位影响(设置是 top、left 等属性无效),当该元素的位置将要移出偏移范围时,定位又会变成 fixed,根据设置的 left、top 等属性成固定位置的效果。

float

  • float:left (或 right),向左(或右)浮动,直到它的边缘碰到包含框或另一个浮动框为止。

且脱离普通的文档流,会被正常文档流内的块框忽略。不占据空间,无法将父类元素撑开。

  • 任何元素都可以浮动,浮动元素会生成一个块级框,不论它本身是何种元素。因此,没有必要为浮动元素设置 display:block。
  • 如果浮动非替换元素,则要指定一个明确的 width,否则它们会尽可能的窄。

什么叫替换元素 ?根据元素本身的特点定义的, (X)HTML中的 img、input、textarea、select、object 都是替换元素,这些元素都没有实际的内容。 (X)HTML 的大多数元素是不可替换元素,他们将内容直接告诉浏览器,将其显示出来。

display

  • display 属性取值:none、inline、inline-block、block、table 相关属性值、inherit。
  • display 属性规定元素应该生成的框的类型。文档内任何元素都是框,块框或行内框。
  • display:none 和 visiability:hidden 都可以隐藏 div,区别有点像 absolute 和 relative,前者不占据文档的空间,后者还是占据文档的位置。
  • display:inline 和 block,又叫行内元素和块级元素。

表现出来的区别就是 block 独占一行,在浏览器中通常垂直布局,可以用 margin 来控制块级元素之间的间距(存在 margin 合并的问题,只有普通文档流中块框的垂直外边距才会发生外边距合并。行内框、浮动框或绝对定位之间的外边距不会合并。);
而 inline 以水平方式布局,垂直方向的 margin 和 padding 都是无效的,大小跟内容一样,且无法设置宽高。
inline 就像塑料袋,内容怎么样,就长得怎么样;block 就像盒子,有固定的宽和高。

  • inline-block 就介于两者之间。
  • table 相关的属性值可以用来垂直居中,效果一般。
  • flex

定位机制

上面三个属性都属于 CSS 定位属性。CSS 三种基本的定位机制:普通流、浮动、绝对定位。


css3 动画效果属性有哪些 ?

  • animation-name:规定需要绑定到选择器的 keyframe 名称。。
  • animation-duration:规定完成动画所花费的时间,以秒或毫秒计。
  • animation-timing-function:规定动画的速度曲线。
  • animation-delay:规定在动画开始之前的延迟。
  • animation-iteration-count:规定动画应该播放的次数。
  • animation-direction:规定是否应该轮流反向播放动画。

canvas 与 svg 的区别 ?

  • Canvas 是基于像素的即时模式图形系统,最适合较小的表面或较大数量的对象,Canvas 不支持鼠标键盘等事件。
  • SVG 是基于形状的保留模式图形系统,更加适合较大的表面或较小数量的对象。
  • Canvas 和 SVG 在修改方式上还存在着不同。绘制 Canvas 对象后,不能使用脚本和 CSS 对它进行修改。因为 SVG 对象是文档对象模型的一部分,所以可以随时使用脚本和 CSS 修改它们。

现在对两种技术做对比归纳如下:

Canvas

1) 依赖分辨率
2) 不支持事件处理器
3) 弱的文本渲染能力
4) 能够以 .png 或 .jpg 格式保存结果图像
5) 最适合图像密集型的游戏,其中的许多对象会被频繁重绘

SVG

1) 不依赖分辨率
2) 支持事件处理器
3) 最适合带有大型渲染区域的应用程序(比如谷歌地图)
4) 复杂度高会减慢渲染速度(任何过度使用 DOM 的应用都不快)
5) 不适合游戏应用


px 和 em 的区别 ?

  • px 像素(Pixel)。相对长度单位。像素 px 是相对于显示器屏幕分辨率而言的。(引自CSS2.0手册)
  • em 是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸。(引自CSS2.0手册)
  • 任意浏览器的默认字体高都是 16px。所有未经调整的浏览器都符合:1em = 16px。

那么12px = 0.75em,10px = 0.625em。为了简化 font-size 的换算,需要在 css 中的 body 选择器中声明 Font-size = 62.5%,这就使 em 值变为 16px*62.5% = 10px, 这样 12px = 1.2em, 10px = 1em, 也就是说只需要将你的原来的 px 数值除以 10,然后换上 em 作为单位就行了。


会不会用 ps 扣图,png、jpg、gif 这些图片格式解释一下,分别什么时候用。如何优化图像、图像格式的区别 ?

JPG 的特性

  • 支持摄影图像或写实图像的高级压缩,并且可利用压缩比例控制图像文件大小。
  • 有损压缩会使图像数据质量下降,并且在编辑和重新保存 JPG 格式图像时,这种下降损失会累积。
  • JPG 不适用于所含颜色很少、具有大块颜色相近的区域或亮度差异十分明显的较简单的图片。

PNG 的特性

  • 能在保证最不失真的情况下尽可能压缩图像文件的大小。
  • PNG 用来存储灰度图像时,灰度图像的深度可多到 16 位,存储彩色图像时,彩色图像的深度可多到 48 位,并且还可存储多到 16 位的 α 通道数据。
  • 对于需要高保真的较复杂的图像,PNG 虽然能无损压缩,但图片文件较大,不适合应用在 Web 页面上。
  • 另外还有一个原则就是用于页面结构的基本视觉元素,如容器的背景、按钮、导航的背景等应该尽量用 PNG 格式进行存储,这样才能更好的保证设计品质。而其他一些内容元素,如广告 Banner、商品图片 等对质量要求不是特别苛刻的,则可以用 JPG 去进行存储从而降低文件大小。

GIF格式特点
 

  • 透明性: Gif 是一种布尔透明类型,既它可以是全透明,也可以是全不透明,但是它并没有半透明(alpha 透明)。 
  • 动画:Gif 这种格式支持动画。 
  • 无损耗性:Gif 是一种无损耗的图像格式,这也意味着你可以对 gif 图片做任何操作也不会使得图像质量产生损耗。 
  • 水平扫描:Gif 是使用了一种叫作 LZW 的算法进行压缩的,当压缩 gif 的过程中,像素是由上到下水平压缩的,这也意味着同等条件下,横向的 gif 图片比竖向的 gif 图片更加小。

例如 50010 的图片比 10500 的图片更加小。
间隔渐进显示:Gif 支持可选择性的间隔渐进显示 

由以上特点看出只有 256 种颜色的 gif 图片不适合作为照片,它适合做对颜色要求不高的图形。


我们知道可以以外链的方式引入 CSS 文件,请谈谈外链引入 CSS 有哪些方式,这些方式的性能有区别吗 ?

CSS 的引入方式最常用的有三种

第一:外链式

这种方法可以说是现在占统治地位的引入方法。

如同 IE 与浏览器。这也是最能体现 CSS 特点的方法;

最能体现 DIV + CSS 中的内容离的思想,也最易改版维护,代码看起来也是最美观的一种。

第二:内部样式表

这种方法的使用情况要少的多,最长见得就是访问量大的门户网站。或者访问量较大的企业网站的首页。

与第一种方法比起来,优弊端也明显。

优点:速度快,所有的 CSS 控制都是针对本页面标签的,没有多余的 CSS 命令;再者不用外链 CSS 文件。直接在文档中读取样式。

缺点:就是改版麻烦些,单个页面显得臃肿,CSS 不能被其他 HTML 引用造成代码量相对较多,维护也麻烦些采用这种方法的公司大多有钱,对他们来说用户量是关键,他们不缺人进行复杂的维护工作。

第三:行内样式

认为 HTML 里不能出现 CSS 命令。其实有时候没有什么大不了。比如通用性差,效果特殊,使用 CSS 命令较少,并且不常改动的地方,使用这种方法反而是很好的选择。

第四、@import 引入方式

<style type="text/css">
@import url(my.css);
</style>

CSS Sprite 是什么,谈谈这个技术的优缺点。

加速的关键,不是降低重量,而是减少个数。传统切图讲究精细,图片规格越小越好,重量越小越好,其实规格大小无计算机统一都按 byte 计算。客户端每显示一张图片都会向服务器发送请求。所以,图片越多请求次数越多,造成延迟的可越大。

  • 利用 CSS Sprites 能很好地减少了网页的 http 请求,从而大大的提高了页面的性能,这也是 CSS Sprites 的优点,也是其被广泛传播和应用的主要原因;
  • CSS Sprites 能减少图片的字节,曾经比较过多次 3 张图片合并成 1 张图片的字节总是小于这 3 张图片的和。
  • 解决了网页设计师在图片命名上的困扰,只需对一张集合的图片上命名就可以了,不需要对每一个小元素名,从而提高了网页的制作效率。
  • 更换风格方便,只需要在一张或少张图片上修改图片的颜色或样式,整个网页的风格就可以改变。维护起方便。

诚然 CSS Sprites 是如此的强大,但是也存在一些不可忽视的缺点,如下:

  • 在图片合并的时候,你要把多张图片有序的合理的合并成一张图片,还要留好足够的空间,防止板块内不不必要的背景;这些还好,最痛苦的是在宽屏,高分辨率的屏幕下的自适应页面,你的图片如果不够宽,很容背景断裂;
  • CSS Sprites 在开发的时候比较麻烦,你要通过 photoshop 或其他工具测量计算每一个背景单元的精确位是针线活,没什么难度,但是很繁琐;幸好腾讯的鬼哥用 RIA 开发了一个 CSS Sprites 样式生成工具,虽然些使用上的不灵活,但是已经比 photoshop 测量来的方便多了,而且样式直接生成,复制,拷贝就 OK!
  • CSS Sprites 在维护的时候比较麻烦,如果页面背景有少许改动,一般就要改这张合并的图片,无需改的好不要动,这样避免改动更多的 css,如果在原来的地方放不下,又只能(最好)往下加图片,这样图片的字加了,还要改动 css。

CSS Sprites 非常值得学习和应用,特别是页面有一堆 ico(图标)。总之很多时候大家要权衡一下再决定是不是应用 CSS Sprites。


以 CSS3 标准定义一个 webkit 内核浏览器识别的圆角(尺寸随意)

-moz-border-radius: 10px; 
-webkit-border-radius: 10px;
 border-radius: 10px;。

优先级算法如何计算?内联和 important 哪个优先级高 ?

  • 优先级就近原则,样式定义最近者为准
  • 载入样式以最后载入的定位为准
  • 优先级为 !important > [ id > class > tag ]
  • Important 比内联优先级高

css 的基本语句构成是 ?

回答:选择器、属性和属性值。


如果让你来制作一个访问量很高的大型网站,你会如何来管理所有 CSS 文件、JS 与图片?

回答:涉及到人手、分工、同步;

  • 先期团队必须确定好全局样式(globe.css),编码模式 (utf-8) 等
  • 编写习惯必须一致(例如都是采用继承式的写法,单样式都写成一行);
  • 标注样式编写人,各模块都及时标注(标注关键样式调用的地方);
  • 页面进行标注(例如页面模块开始和结束);
  • CSS 跟 HTML 分文件夹并行存放,命名都得统一(例如 style.css)
  • JS 分文件夹存放,命名以该 JS 功能为准
  • 图片采用整合的 png8 格式文件使用,尽量整合在一起使用,方便将来的管理。

CSS 选择符有哪些 ?哪些属性可以继承 ?优先级算法如何计算 ?新增伪类有那些 ?

CSS 选择符

  1. id 选择器( #myid)
  2. 类选择器(.myclassname)
  3. 标签选择器(div, h1, p)
  4. 相邻选择器(h1 + p)
  5. 子选择器(ul > li)
  6. 后代选择器(li a)
  7. 通配符选择器( * )
  8. 属性选择器(a[rel = "external"])
  9. 伪类选择器(a: hover, li: nth - child)

可继承的样式

font-size,font-family,color,ul,li,dl,dd,dt;

不可继承的样式

border padding margin width height
事实上,宽度也不是继承的,而是如果你不指定宽度,那么它就是 100%。由于你子 DIV 并没有指定宽度,那它就是 100%,也就是与父 DIV 同宽,但这与继承无关,高度自然也没有继承一说。

优先级算法

  • 优先级就近原则,同权重情况下样式定义最近者为准;
  • 载入样式以最后载入的定位为准;
  • 优先级为: !important > id > class > tag , important 比 内联优先级高

CSS3 新增伪类举例

  • :root 选择文档的根元素,等同于 html 元素
  • :empty 选择没有子元素的元素
  • :target 选取当前活动的目标元素
  • :not(selector) 选择除 selector 元素意外的元素
  • :enabled 选择可用的表单元素
  • :disabled 选择禁用的表单元素
  • :checked 选择被选中的表单元素
  • :after 选择器在被选元素的内容后面插入内容
  • :before 选择器在被选元素的内容前面插入内容
  • :nth-child(n) 匹配父元素下指定子元素,在所有子元素中排序第 n
  • :nth-last-child(n) 匹配父元素下指定子元素,在所有子元素中排序第 n,从后向前数
  • :nth-child(odd) 奇数
  • :nth-child(even) 偶数
  • :nth-child(3n+1)
  • :first-child
  • :last-child
  • :only-child
  • :nth-of-type(n) 匹配父元素下指定子元素,在同类子元素中排序第 n
  • :nth-last-of-type(n) 匹配父元素下指定子元素,在同类子元素中排序第 n,从后向前数
  • :nth-of-type(odd)
  • :nth-of-type(even)
  • :nth-of-type(3n+1)
  • :first-of-type
  • :last-of-type
  • :only-of-type
  • ::selection 选择被用户选取的元素部分
  • :first-line 选择元素中的第一行
  • :first-letter 选择元素中的第一个字符

CSS3 有哪些新特性 ?

  • CSS3 实现圆角(border-radius:8px)
  • 阴影(box-shadow:10px)
  • 对文字加特效(text-shadow)
  • 线性渐变(gradient)
  • 旋转、缩放、定位、倾斜
 transform: rotate(9deg) scale(0.85,0.90) translate(0px,-30px) skew(-9deg,0deg); 
  • 增加了更多的 CSS 选择器
  • 多背景 rgba

一个满屏 品字布局 如何设计 ?

第一种方式

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>满屏品字布局</title>
    <style type="text/css">
        *{
            margin: 0;
            padding: 0;
        }
        html,body{
            height: 100%;/*此设置非常关键,因为默认的body,HTML高度为0,所以后面设置的div的高度无法用百分比显示*/
        }       
        .header{
            height:50%; /*此步结合html,body高度为100%,解决元素相对窗口的定位问题*/
            width: 50%;     
            background: #ccc;           
            margin:0 auto;
        }
        .main{
            width: 100%;
            height: 50%;
            background: #ddd;
        }
        .main .left,.main .right{
            float: left;/*采用float方式,对元素进行左右定位*/
            width:50%;/*此步解决元素相对窗口的定位问题*/
            height:100%;/*此步解决元素相对窗口的定位问题*/
            background: yellow;
        }
        .main .right{
            background: green;
        }
    </style>
</head>
<body>
<div class="header"></div>
<div class="main">
    <div class="left"></div>
    <div class="right"></div>
</div>
</body>
</html>

效果如下: 

clipboard.png


为什么要初始化 CSS 样式 ?

因为浏览器的兼容问题,不同浏览器对有些标签的默认值是不同的,如果没对 CSS 初始化往往会出现浏览器之间的页面显示差异。
初始化样式会对 SEO 有一定的影响,但鱼和熊掌不可兼得,但力求影响最小的情况下初始化。

初始化 CSS 样式例子

html,
body {
  padding: 0; 
  margin: 0;
} 
...

collapse、overflow、float 这些特性相互叠加后会怎么样?

margin collapse 我觉得这里的意思应该是 Collapsing margins,即外边距折叠,指的是毗邻的两个或多个外边距 (margin) 会合并成一个外边距。

其中所说的 margin 毗邻,可以归结为以下两点:

  • 这两个或多个外边距没有被非空内容、padding、border 或 clear 分隔开。
  • 这些 margin 都处于普通流中。
  1. 两个或多个毗邻的普通流中的块元素垂直方向上的 margin 会折叠。
  2. 浮动元素、inline-block 元素、绝对定位元素的 margin 不会和垂直方向上其他元素的 margin 折叠.
  3. 创建了块级格式化上下文的元素,不和它的子元素发生 margin 折叠

请解释一下CSS3 的 Flexbox(弹性盒布局模型),以及适用场景 ?

http://www.ruanyifeng.com/blo...

任何一个容器都可以指定为 Flex 布局,行内元素也可以使用 Flex 布局。

注意:设为 Flex 布局以后,子元素的 float、clear 和 vertical-align 属性将失效。


flex 布局最常用的是什么场景 ?

一般实现垂直居中是一件很麻烦的事,但 flex 布局轻松解决。

.demo {
  display: flex;            
  justify-content: center;                    
  align-items: center;
}

用纯 CSS 创建一个三角形的原理是什么?

把上、左、右三条边隐藏掉(颜色设为 transparent)

#demo {
 width: 0;
 height: 0;
 border-width: 20px;
 border-style: solid;
 border-color: transparent transparent red transparent;
}

absolute 的 containing block(容器块) 计算方式跟正常流有什么不同 ?

无论属于哪种,都要先找到其祖先元素中最近的 position 值不为 static 的元素,然后再判断:

  • 若此元素为 inline 元素,则 containing block 为能够包含这个元素生成的第一个和最后一个 inline box 的 padding box (除 margin,border 外的区域) 的最小矩形;
  • 否则,则由这个祖先元素的 padding box 构成。
  • 如果都找不到,则为 initial containing block。

补充:

  1. static / relative:简单说就是它的父元素的内容框(即去掉 padding 的部分)
  2. absolute: 向上找最近的定位为 absolute / relative 的元素
  3. fixed: 它的 containing block 一律为根元素(html / body),根元素也是 initial containing block

 

对 BFC 规范(块级格式化上下文:blockformatting context)的理解 ?

W3C CSS 2.1 规范中的一个概念,它是一个独立容器,决定了元素如何对其内容进行定位,以及与其他元素的关系和相互作用。

  • 一个页面是由很多个 Box 组成的,元素的类型和 display 属性,决定了这个 Box 的类型。
  • 不同类型的 Box,会参与不同的 Formatting Context(决定如何渲染文档的容器),因此 Box 内的元素会以不同的方式渲染,也就是说 BFC 内部的元素和外部的元素不会互相影响。

用 position: absolute 跟用 float 有什么区别吗 ?

  • 都是脱离标准流,只是 position: absolute 定位用的时候,位置可以给的更精确(想放哪就放哪),而 float 用的更简洁,向右,左,两个方向浮动,用起来就一句代码。
  • 还有就是 position: absolute 不管在哪个标签里,都可以定位到任意位置,毕竟 top,left,bottom,right 都可以给正值或负值;
  • float 只是向左或向右浮动,不如 position: absolute 灵活,浮动后再想改变位置就要加各种 margin,padding 之类的通过间距的改变来改变位置,我自身觉得这样的话用起来不方便,也不太好。
  • 但在菜单栏,或者一些图标的横向排列时,用起来特别方便,一个 float 就解决了,而且每个元素之间不会有任何间距(所以可以用 float 消除元素间的距离);

svg 与 convas 的区别 ?

  • svg 绘制出来的每一个图形的元素都是独立的 DOM 节点,能够方便的绑定事件或用来修改,而 canvas 输出的是一整幅画布;
  • svg 输出的图形是矢量图形,后期可以修改参数来自由放大缩小,不会是真和锯齿。而 canvas 输出标量画布,就像一张图片一样,放大会失真或者锯齿。

何时应当时用 padding 和 margin ?

何时应当使用 margin

  • 需要在 border 外侧添加空白时。
  • 空白处不需要背景(色)时。
  • 上下相连的两个盒子之间的空白,需要相互抵消时。

如 15px + 20px 的 margin,将得到 20px 的空白。

何时应当时用 padding

  • 需要在 border 内测添加空白时。
  • 空白处需要背景(色)时。
  • 上下相连的两个盒子之间的空白,希望等于两者之和时。

如 15px + 20px 的 padding,将得到 35px 的空白。

个人认为:margin 是用来隔开元素与元素的间距;padding 是用来隔开元素与内容的间隔,让内容(文字)与(包裹)元素之间有一段 呼吸距离


文字在超出长度时,如何实现用省略号代替 ? 超长长度的文字在省略显示后,如何在鼠标悬停时,以悬浮框的形式显示出全部信息 ?

注意:设置 width,overflow: hidden, white-space: nowrap (规定段落中的文本不进行换行), text-overflow: ellipsis,四个属性缺一不可。这种写法在所有的浏览器中都能正常显示。


CSS 里的 visibility 属性有个 collapse 属性值 ?在不同浏览器下有什么区别 ?

Collapse

  • 当在表格元素中使用时,此值可删除一行或一列,但是它不会影响表格的布局,被行或列占据的空间会留给其他内容使用。
  • 如果此值被用在其他的元素上,会呈现为 hidden。
  • 当一个元素的 visibility 属性被设置成 collapse 值后,对于一般的元素,它的表现跟 hidden 是一样的。
  • chrome中,使用 collapse 值和使用 hidden 没有区别。
  • firefox,opera 和 IE,使用 collapse 值和使用 display:none 没有什么区别。

position 跟 display、overflow、float 这些特性相互叠加后会怎么样 ?

  • display 属性规定元素应该生成的框的类型;
  • position 属性规定元素的定位类型;
  • float 属性是一种布局方式,定义元素在哪个方向浮动。
  • 类似于优先级机制:position:absolute / fixed 优先级最高,有他们在时,float 不起作用,display 值需要调整。float 或者 absolute 定位的元素,只能是块元素或表格。

对 BFC 规范(块级格式化上下文:block formatting context) 的理解 ?

BFC 规定了内部的 Block Box 如何布局。

定位方案:

  • 内部的 Box 会在垂直方向上一个接一个放置。
  • Box 垂直方向的距离由 margin 决定,属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠。
  • 每个元素的 margin box 的左边,与包含块 border box 的左边相接触。
  • BFC 的区域不会与 float box 重叠。
  • BFC 是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。

计算 BFC 的高度时,浮动元素也会参与计算。

满足下列条件之一就可触发 BFC:

  • 1、根元素,即 html
  • 2、float 的值不为 none(默认)
  • 3、overflow 的值不为 visible(默认)
  • 4、display 的值为 inline-block、table-cell、table-caption
  • 5、position 的值为 absolute 或 fixed

浏览器是怎样解析 CSS 选择器的 ?

  • CSS 选择器的解析是从右向左解析的。
  • 若从左向右的匹配,发现不符合规则,需要进行回溯,会损失很多性能。
  • 若从右向左匹配,先找到所有的最右节点,对于每一个节点,向上寻找其父节点直到找到根元素或满足条件的匹配规则,则结束这个分支的遍历。
  • 两种匹配规则的性能差别很大,是因为从右向左的匹配在第一步就筛选掉了大量的不符合条件的最右节点(叶子节点),而从左向右的匹配规则的性能都浪费在了失败的查找上面。
  • 而在 CSS 解析完毕后,需要将解析的结果与 DOM Tree 的内容一起进行分析建立一棵 Render Tree,最终用来进行绘图。
  • 在建立 Render Tree 时(WebKit 中的「Attachment」过程),浏览器就要为每个 DOM Tree 中的元素根据 CSS 的解析结果(Style Rules)来确定生成怎样的 Render Tree。

元素竖向的百分比设定是相对于容器的高度吗 ?

当按百分比设定一个元素的宽度时,它是相对于父容器的宽度计算的。


全屏滚动的原理是什么 ?用到了 CSS 的哪些属性 ?

原理

  • 有点类似于轮播,整体的元素一直排列下去,假设有 5 个需要展示的全屏页面,那么高度是 500%,只是展示 100%,剩下的可以通过 transform 进行 y 轴定位,也可以通过
    margin-top 实现。
  • overflow:hidden;transition:all 1000ms ease;

什么是响应式设计 ?响应式设计的基本原理是什么 ?如何兼容低版本的 IE ?

  • 响应式网站设计( Responsive Web design ) 是一个网站能够兼容多个终端,而不是为每一个终端做一个特定的版本。
  • 基本原理是通过媒体查询检测不同的设备屏幕尺寸做处理。
  • 页面头部必须有 meta 声明的 viewport。
<meta name="viewport" content="” width="device-width" initial-scale="1" maximum-scale="1" user-scalable="no"/>

视差滚动效果 ?

视差滚动(Parallax Scrolling)通过在网页向下滚动的时候,控制背景的移动速度比前景的移动速度慢 来创建出令人惊叹的 3D 效果。

  • CSS3 实现。

优点:开发时间短、性能和开发效率比较好,缺点是不能兼容到低版本的浏览器

  • jQuery 实现。

通过控制不同层滚动速度,计算每一层的时间,控制滚动效果。优点:能兼容到各个版本的,效果可控性好。缺点:开发起来对制作者要求高。

  • 插件实现方式。

例如:parallax-scrolling,兼容性十分好。


::before 和 :after 中双冒号和单冒号有什么区别 ?解释一下这 2 个伪元素的作用

  • 单冒号 (:) 用于 CSS3 伪类,双冒号 (::) 用于 CSS3 伪元素。
  • ::before 就是以一个子元素的存在,定义在元素主体内容之前的一个伪元素。并不存在于 dom 之中,只存在在页面之中。

:before 和 :after 这两个伪元素,是在 CSS2.1 里新出现的。
起初,伪元素的前缀使用的是单冒号语法,但随着 Web 的进化,在 CSS3 的规范里,伪元素的语法被修改成使用双冒号,成为 ::before、 ::after 。


怎么让 Chrome 支持小于 12px 的文字 ?

p {
  font-size: 10px;
  -webkit-transform: scale(0.8);  // 0.8 是缩放比例
} 

让页面里的字体变清晰,变细用 CSS 怎么做 ?

-webkit-font-smoothing 在 window 系统下没有起作用,但是在 IOS 设备上起作用 -webkit-font-smoothing:antialiased 是最佳的,灰度平滑。


如果需要手动写动画,你认为最小时间间隔是多久,为什么 ?

多数显示器默认频率是 60Hz,即 1 秒刷新 60 次,所以理论上最小间隔为:1/60*1000ms = 16.7ms。


有一个高度自适应的 div,里面有两个 div,一个高度 100px,如何让另一个填满剩下的高度 ?

  • 外层 div 使用 position:relative;
  • 高度要求自适应的 div 使用 position: absolute; top: 100px; bottom: 0; left: 0

png、jpg、gif 这些图片格式解释一下,分别什么时候用。有没有了解过webp ?

  • png 是便携式网络图片(Portable Network Graphics)是一种无损数据压缩位图文件格式。

优点是:压缩比高,色彩好。 大多数地方都可以用。

  • jpg 是一种针对相片使用的一种失真压缩方法,是一种破坏性的压缩,在色调及颜色平滑变化做的不错。在 www 上,被用来储存和传输照片的格式。
  • gif 是一种位图文件格式,以 8 位色重现真色彩的图像。可以实现动画效果。
  • webp 格式是谷歌在 2010 年推出的图片格式,压缩率只有 jpg 的 2/3,大小比 png 小了 45%。缺点是压缩的时间更久了,兼容性不好,目前谷歌和 opera 支持。

style 标签写在 body 后与 body 前有什么区别?

页面加载自上而下,当然是先加载样式。

写在 body 标签后,由于浏览器以逐行方式对 HTML 文档进行解析,当解析到写在尾部的样式表(外联或写在style标签)会导致浏览器停止之前的渲染,等待加载且解析样式表完成之后重新渲染,在 windows 的 IE 下可能会出现 FOUC 现象(即样式失效导致的页面闪烁问题)


阐述一下CSS Sprites

将一个页面涉及到的所有图片都包含到一张大图中去,然后利用 CSS 的 background-image,background-repeat,background-position 的组合进行背景定位。

利用 CSS Sprites 能很好地减少网页的 http 请求,从而大大的提高页面的性能;
CSS Sprites 能减少图片的字节。


用 css 实现左侧宽度自适应,右侧固定宽度 ?

1、标准浏览器的方法

当然,以不折腾人为标准的 w3c 标准早就为我们提供了制作这种自适应宽度的标准方法。

  • 把 container 设为 display: table 并指定宽度 100%;
  • 然后把 main + sidebar 设为 display: table-cell;
  • 然后只给 sidebar 指定一个宽度,那么 main 的宽度就变成自适应了。

代码很少,而且不会有额外标签。不过这是 IE7 及以下都无效的方法。

.container {
    display: table;
    width: 100%;
}
.main {
    display: table-cell;
}
.sidebar {
    display: table-cell;
    width: 300px;
}

clipboard.png

2、固定区域浮动,自适应区域不设置宽度但设置 margin

.container {
    overflow: hidden;
    *zoom: 1;
}
.sidebar {
    float: right;
    width: 300px;
    background: #333;
}
.main {
    margin-right: 320px;
    background: #666;
}
.footer {
    margin-top: 20px;
    background: #ccc;
}

其中,sidebar 让它浮动,并设置了一个宽度;而 main 没有设置宽度。

大家要注意 html 中必须使用 div 标签,不要妄图使用什么 p 标签来达到目的。因为 div 有个默认属性,即如果不设置宽度,那它会自动填满它的父标签的宽度。这里的 main 就是例子。

当然我们不能让它填满了,填满了它就不能和 sidebar 保持同一行了。我们给它设置一个 margin。由于 sidebar 在右边,所以我们设置 main 的 margin-right 值,值比 sidebar 的宽度大一点点——以便区分它们的范围,例子中是 320。

假设 main 的默认宽度是 100%,那么它设置了 margin 后,它的宽度就变成了 100% - 320,此时 main 发现自己的宽度可以与 sidebar 挤在同一行了,于是它就上来了。
而宽度 100% 是相对于它的父标签来的,如果我们改变了它父标签的宽度,那 main 的宽度也就会变——比如我们把浏览器窗口缩小,那 container 的宽度就会变小,而 main 的宽度也就变小,但它的实际宽度 100% - 320 始终是不会变的。

这个方法看起来很完美,只要我们记得清除浮动(这里我用了最简单的方法),那 footer 也不会错位。而且无论 main 和 sidebar 谁更长,都不会对布局造成影响。

但实际上这个方法有个很老火的限制——html 中 sidebar 必须在 main 之前!
但我需要 sidebar 在 main 之后!因为我的 main 里面才是网页的主要内容,我不想主要内容反而排在次要内容后面。
但如果 sidebar 在 main 之后,那上面的一切都会化为泡影。

clipboard.png

3、固定区域使用定位,自适应区域不设置宽度,但设置 margin

.container {
    position: relative;
}
.sidebar {
    position: absolute;
    top: 0;
    right: 0;
    width: 300px;
    background: #333;
}
.main {
    margin-right: 320px;
    background: #666;
}

clipboard.png

咦,好像不对,footer 怎么还是在那儿呢?怎么没有自动往下走呢?footer 说——我不给绝对主义者让位!
其实这与 footer 无关,而是因为 container 对 sidebar 的无视造成的——你再长,我还是没感觉。
看来这种定位方式只能满足 sidebar 自己,但对它的兄弟们却毫无益处。

4、左边浮动,右边 overflow: hidden;

*{ margin:0; padding: 0; }
html,body{
   height: 100%;/*高度百分百显示*/
}
#left {
    width: 300px;
    height: 100%;
    background-color: #ccc;
    float: left;
}
#right {
    height: 100%;
    overflow: hidden;
    background-color: #eee;
}

第二种方法,我利用的是创建一个新的 BFC(块级格式化上下文)来防止文字环绕的原理来实现的。

BFC 就是一个相对独立的布局环境,它内部元素的布局不受外面布局的影响。
它可以通过以下任何一种方式来创建: 

  • float 的值不为 none 
  • position 的值不为 static 或者 relative 
  • display 的值为 table-cell , table-caption , inline-block , flex , 或者 inline-flex 中的其中一个 overflow 的值不为 visible

关于 BFC,在 w3c 里是这样描述的:在 BFC 中,每个盒子的左外边框紧挨着 包含块 的 左边框 (从右到左的格式化时,则为右边框紧挨)。
即使在浮动里也是这样的(尽管一个包含块的边框会因为浮动而萎缩),除非这个包含块的内部创建了一个新的 BFC。
这样,当我们给右侧的元素单独创建一个 BFC 时,它将不会紧贴在包含块的左边框,而是紧贴在左元素的右边框。


问:浮动的原理和工作方式,会产生什么影响呢,要怎么处理 ?

工作方式:浮动元素脱离文档流,不占据空间。浮动元素碰到包含它的边框或者浮动元素的边框停留。

影响

  • 浮动会导致父元素无法被撑开,影响与父元素同级的元素。
  • 与该浮动元素同级的非浮动元素,如果是块级元素,会移动到该元素下方,而块级元素内部的行内元素会环绕浮动元素;而如果是内联元素则会环绕该浮动元素。
  • 与该元素同级的浮动元素,对于同一方向的浮动元素(同级),两个元素将会跟在碰到的浮动元素后面;而对于不同方向的浮动元素,在宽度足够时,将分别浮动向不同方向,在宽度不同是将导致一方换行(换行与 HTML 书写顺序有关,后边的将会浮动到下一行)。
  • 浮动元素将被视作为块元素。
  • 而浮动元素对于其父元素之外的元素,如果是非浮动元素,则相当于忽视该浮动元素,如果是浮动元素,则相当于同级的浮动元素。
  • 而常用的清除浮动的方法,则如使用空标签,overflow,伪元素等。

在使用基于浮动设计的 CSS 框架时,自会提供清除的方法,个人并不习惯使用浮动进行布局。


对 CSS Grid 布局的使用

5 分钟学会 CSS Grid 布局


rem、em、px、vh 与 vw 的区别 ?

一、 rem 的特点

  1. rem 的大小是根据 html 根目录下的字体大小进行计算的。
  2. 当我们改变根目录下的字体大小的时候,下面字体都改变。
  3. rem 不仅可以设置字体的大小,也可以设置元素宽、高等属性。
  4. rem 是 CSS3 新增的一个相对单位(root em,根em),这个单位与 em 区别在于使用 rem 为元素设定字体大小时,仍然是相对大小,但相对的只是 HTML 根元素。

这个单位可谓集相对大小和绝对大小的优点于一身,通过它既可以做到只修改根元素就成比例地调整所有字体大小,又可以避免字体大小逐层复合的连锁反应。
目前,除了 IE8 及更早版本外,所有浏览器均已支持 rem。
对于不支持它的浏览器,应对方法也很简单,就是多写一个绝对单位的声明。这些浏览器会忽略用 rem 设定的字体大小。

二、px 特点

  1. px 像素(Pixel)。相对长度单位。像素 px 是相对于显示器屏幕分辨率而言的。

三、em 特点 

  1. em 的值并不是固定的;
  2. em 会继承父级元素的字体大小。
  3. em 是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸。
  4. 任意浏览器的默认字体高都是 16px。

所有未经调整的浏览器一般都符合: 1em = 16px。那么 12px = 0.75em,10px = 0.625em。
为了简化 font-size 的换算,需要在 css 中的 body 选择器中声明 Fontsize = 62.5%,这就使 em 值变为 16px*62.5% = 10px, 这样 12px = 1.2em, 10px = 1em, 也就是说只需要将你的原来的 px 数值除以 10,然后换上 em 作为单位就行了。

四、vh 与 vw

视口

  • 在桌面端,指的是浏览器的可视区域;
  • 在移动端,它涉及 3个 视口:Layout Viewport(布局视口),Visual Viewport(视觉视口),Ideal Viewport(理想视口)。
  • 视口单位中的 “视口”,桌面端指的是浏览器的可视区域;移动端指的就是 Viewport 中的 Layout Viewport。

vh / vw 与 %

单位 : 解释

  • vw :1vw = 视口宽度的 1%
  • vh :1vh = 视口高度的 1%
  • vmin :选取 vw 和 vh 中最小的那个
  • vmax :选取 vw 和 vh 中最大的那个

比如:浏览器视口尺寸为 370px,那么 1vw = 370px * 1% = 6.5px (浏览器会四舍五入向下取 7)

vh / vw 与 % 区别

单位 :解释

  • % :元素的祖先元素
  • vh / vw :视口的尺寸

不过由于 vw 和 vh 是 css3 才支持的长度单位,所以在不支持 css3 的浏览器中是无效的。


什么叫优雅降级和渐进增强 ?

  • 渐进增强 progressive enhancement:针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进和追加功能达到更好的用户体验。
  • 优雅降级 graceful degradation:一开始就构建完整的功能,然后再针对低版本浏览器进行兼容。

区别

  • 优雅降级是从复杂的现状开始,并试图减少用户体验的供给;
  • 渐进增强则是从一个非常基础的,能够起作用的版本开始,并不断扩充,以适应未来环境的需要;
  • 降级(功能衰减)意味着往回看;而渐进增强则意味着朝前看,同时保证其根基处于安全地带。

最后

前端硬核面试专题的完整版在此:前端硬核面试专题,包含:HTML + CSS + JS + ES6 + Webpack + Vue + React + Node + HTTPS + 数据结构与算法 + Git 。

如果觉得本文还不错,记得给个 star , 你的 star 是我持续更新的动力!。

clipboard.png

查看原文

赞 39 收藏 29 评论 2

2FPS 赞了文章 · 2019-07-30

从零开始,揭秘React服务端渲染核心技术

抛砖引玉

在早几年前,jquery算是一个前端工程师必备的技能。当时很多公司采用的是java结合像velocity或者freemarker这种模板引擎的开发模式,页面渲染这块交给了服务器,而前端人员负责用jquery去写一些交互以及业务逻辑。但是随着像react和vue这类框架的大火,这种结合模版引擎的服务端渲染模式逐渐地被抛弃了,而选用了客户端渲染的模式。这样带来的直接好处就是,减少了服务器的压力以及带来了更好的用户体验,尤其在页面切换的过程,客户端渲染的用户体验那是比服务端渲染好太多了。但是随着时间的变化,人们发现客户端渲染的seo非常差,另外首屏渲染时间也过长。而恰巧这些又是服务端渲染的优势,难道再回到以前那种模板引擎的时代?历史是不会开倒车的,所以人们开始尝试在服务端去渲染React或者Vue的组件,最终nuxtjs、nextjs这类的服务端渲染框架应运而生了。当然本文的主要目的不是去介绍这些服务端渲染框架,而是介绍其思路,并且在不借助这些服务端渲染框架的情况下,自己动手搭建一个服务端渲染项目,这里我们以React为例,小伙伴们,快跟我跟一起开始探索之旅吧!

简易的React服务端渲染

renderToString

如果是写过React的小伙伴们,相信对react-dom包的render的方法再熟悉不过,一般我们都是这么写:

import {render} from 'react-dom';
import App from './app';
render(<App/>,document.getElementById("root"));

但是我们发现render只是react-dom里的一部分,其他的方法不知道小伙伴们有没有去研究过,比如renderToString这个方法,这个方法在React官方文档上是这么解释的。

Render a React element to its initial HTML. React will return an HTML string. You can use this method to generate HTML on the server and send the markup down on the initial request for faster page loads and to allow search engines to crawl your pages for SEO purposes.

前面两句说的很清楚,React可以将React元素渲染成它的初始化Html,并且返回html字符串。然后画重点“generate HTML on the server”,在服务端生成html,这不就是服务端渲染吗?我们来试一下可不可以。

const express = require('express');
const app = express();
const React = require('react');
const {renderToString} = require('react-dom/server');
const App = class extends React.PureComponent{
  render(){
    return React.createElement("h1",null,"Hello World");;
  }
};
app.get('/',function(req,res){
  const content = renderToString(React.createElement(App));
  res.send(content);
});
app.listen(3000);

简单说一下逻辑,首先定义一个App组件,返回的内容很简单,就是“Hello World”,然后调用createElement这个方法去生成一个React元素,最后调用renderToString去根据这个React元素去生成一个html字符串并返回给了浏览器。预计效果是页面显示了“Hello World”,我们接下来验证一下。


结果跟我们预想的一致,显示了“Hello World”,接下来我们看一下网页的源代码。


源代码里也有“Hello World”,如果是客户端渲染的话,是无法看到了Hello World的,就比如掘金这个网站就是典型的客户端渲染,随便打开一篇文章你都无法在网页源代码里看到其文章的内容。我们再回到服务端渲染上,什么叫React的服务端渲染?不就是将react组件的内容可以在网页源代码里看到吗?刚刚这个例子就给我们展现了通过renderToString这个方法去实现服务端渲染。到这里似乎是已经完成了我们的服务端渲染了,但是这才刚刚开始,因为仅仅这样是有问题的,我们接着往下研究!

webpack

通过上面这个例子我们发现了这么几个问题:

  • 不能jsx的语法
  • 只能commonjs这个模块化规范,不能用esmodule

因此,我们需要对我们的服务端代码进行一个webpack打包的操作,服务端的webpack配置需要注意这几点:

  • 一定要有 target:"node" 这个配置项
  • 一定要有webpack-node-externals这个库

所以你的webpack配置一定是这样子的:

const nodeExternals = require('webpack-node-externals');
...
module.exports = {
    ...
    target: 'node', //不将node自带的诸如path、fs这类的包打进去
    externals: [nodeExternals()],//不将node_modules里面的包打进去
    ...
};

同构

我们将前面的例子稍微做一下调整,然后变成如下这个样子:

const express = require('express');
const app = express();
const React = require('react');
const {renderToString} = require('react-dom/server');
const App = class extends React.PureComponent{
  handleClick=(e)=>{
    alert(e.target.innerHTML);
  }
  render(){
    return <h1 onClick={this.handleClick}>Hello World!</h1>;
  }
};
app.get('/',function(req,res){
  const content = renderToString(<App/>);
  console.log(content);
  res.send(content);
});
app.listen(3000);

我们给h1这个标签绑定一个click事件,事件响应也很简单,就是弹出h1标签里的内容。预计效果弹出“Hello World!”,我们执行跑一下。如果你执行之后,你会发现无论你如何点击,都不会弹出“Hello World!”。为什么会这个样子?其实很好理解,renderToString只是返回html字符串,元素对应的js交互逻辑并没有返回给浏览器,因此点击h1标签是无法响应的。如何解决这个问题呢?再说解决方法之前,我们先讲一下“同构”这个概念。何为“同构”,简单来说就是“同种结构的不同表现形态”。在这里我们用更通俗的大白话来解释react同构就是:

同一份react代码在服务端执行一遍,再在客户端执行一遍。

同一份react代码,在服务端执行一遍之后,我们就可以生成相应的html。在客户端执行一遍之后就可以正常响应用户的操作。这样就组成了一个完整的页面。所以我们需要额外的入口文件去打包客户端需要的js交互代码。

import React from 'react';
import {render} from 'react-dom';
import App from './app';
render(<App/>,document.getElementById("root"));

这里就跟我们写客户端渲染代码无差,接着用webpack打包一下,然后再在渲染的html字符串中加入对打包后的js的引用代码。

import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import App from  './src/app';
const app = express();

app.use(express.static("dist"))

app.get('/',function(req,res){
  const content = renderToString(<App/>);
  res.send(`
        <!doctype html>
        <html>
            <title>ssr</title>
            <body>
                <div id="root">${content}</div>
                <script data-original="/client/index.js"></script>
            </body> 
        </html>
    `);
});
app.listen(3000);

这里“/client/index.js”就是我们打包出来的用于客户端执行的js文件,然后我们再来看一下效果,此时页面就可以正常响应我们的操作了。

不过这时的控制台会抛出这样一则警告:

Warning: render(): Calling ReactDOM.render() to hydrate server-rendered markup will stop working in React v17. Replace the ReactDOM.render() call with ReactDOM.hydrate() if you want React to attach to the server HTML.

提醒我们在服务端渲染时用ReactDOM.hydrate()来取代ReactDOM.render(),并警告我们在react17时将不能用ReactDOM.render()去混合服务端渲染出来的标签。

至于ReactDOM.hydrate()和ReactDOM.render()的区别就是:

ReactDOM.render()会将挂载dom节点的所有子节点全部清空掉,再重新生成子节点。而ReactDOM.hydrate()则会复用挂载dom节点的子节点,并将其与react的virtualDom关联上。

从二者的区别我们可以看出,ReactDOM.render()会将服务端做的工作全部推翻重做,而ReactDOM.hydrate()在服务端做的工作基础上再进行深入的操作。显然ReactDOM.hydrate()此时是要比ReactDOM.render()更好。ReactDOM.render()在此时只会显得我们很白痴,做了一大堆无用功。所以我们客户端入口文件调整一下。

import React from 'react';
import {hydrate} from 'react-dom';
import App from './app';
hydrate(<App/>,document.getElementById("root"));

流程图

加入路由

前面我们已经用比较大的篇幅讲明白了服务端渲染的原理,搞清楚了大致的服务端渲染的流程,那么接下来就要讲一下如何给其加入路由。在服务端的时候是需要生成Html的,而不同的访问路径对着不同的组件,因此服务端是需要有一个路由层去帮助我们找到该访问路径所对应的react组件。其次,客户端会进行一个hydrate的操作,也是需要根据访问路径去匹配到对应的react组件的。综上所述,服务端和客户端都是需要路由体现的。客户端的路由相信不用再做过多的赘述了,这里我们就讲一下服务端是如何添加路由的。

StaticRouter

在客户端渲染的项目中,react-router提供了BrowserRouter和HashRouter可供我们选择,而这两个router都有一个共同点就是需要读取地址栏的url,但是在服务端的环境中是没有地址栏的,也就是说没有window.location这个对象,那么BrowserRouter和HashRouter就不能在服务端的环境中去使用,那么这就无解了吗,当然不是!react-router给我们提供了另外一种router叫做StaticRouter,react-router官网给它的描述中有这么一句话。

This can be useful in server-side rendering scenarios when the user isn’t actually clicking around, so the location never actually changes. 

我们画一下重点“be useful in server-side rendering scenarios”,意思很明确,就是为了服务端渲染而打造的。接下来我们就结合StaticRouter是去实现一下。
现在我们有两个页面Login和User,代码如下:

Login:

import React from 'react';
export default class Login extends React.PureComponent{
  render(){
    return <div>登陆</div>
  }
}

User:

import React from 'react';
export default class User extends React.PureComponent{
  render(){
    return <div>用户</div>
  }
}

服务端代码:

import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {StaticRouter,Route} from 'react-router';
import Login from '@/pages/login';
import User from '@/pages/user';
const app = express();
app.use(express.static("dist"))
app.get('*',function(req,res){
    const content = renderToString(<div>
    <StaticRouter location={req.url}>
      <Route exact path="/user" component={User}></Route>
      <Route exact path="/login" component={Login}></Route>
    </StaticRouter>
  </div>);
  res.send(`
        <!doctype html>
        <html>
            <title>ssr</title>
            <body>
                <div id="root">${content}</div>
                <script data-original="/client/index.js"></script>
            </body>
        </html>
    `);
});
app.listen(3000);

最后的效果:

/user:

/login:

前后端路由同构

通过上面的小实验,我们已经掌握了如何在服务端去添加路由,接下来我们需要处理的就是前后端路由同构的问题。由于前后端都需要添加路由,如果两端都各自写一遍的话,费时费力不说,维护还很不方便,所以我们希望一套路由可以在两端都跑通。接下来我们就去实现一下。

思路

这里先说下思路,首先我们肯定不会在服务器端写一遍路由,然后在去客户端写一遍路由,这样费时费力不说,而且不易维护。相信大多数人和我一样都希望少写一些代码,少写代码的第一要义就是把通用的部分给抽出来。那么接下来我们就找一下通用的部分,仔细观察不难发现不管是服务端路由还是客户端路由,路径和组件之间的关系是不变的,a路径对应a组件,b路径对应b组件,所以这里我们希望路径和组件之间的关系可以用抽象化的语言去描述清楚,也就是我们所说路由配置化。最后我们提供一个转换器,可以根据我们的需要去转换成服务端或者客户端路由。

代码

routeConf.js

import Login from '@/pages/login';
import User from '@/pages/user';
import NotFound from '@/pages/notFound';

export default [{
  type:'redirect',
  exact:true,
  from:'/',
  to:'/user'
},{
  type:'route',
  path:'/user',
  exact:true,
  component:User
},{
  type:'route',
  path:'/login',
  exact:true,
  component:Login
},{
  type:'route',
  path:'*',
  component:NotFound
}]

router生成器

import React from 'react';
import { createBrowserHistory } from "history";
import {Route,Router,StaticRouter,Redirect,Switch} from 'react-router';
import routeConf from  './routeConf';

const routes = routeConf.map((conf,index)=>{
  const {type,...otherConf} = conf;
  if(type==='redirect'){
    return <Redirect  key={index} {...otherConf}/>;
  }else if(type ==='route'){
    return <Route  key={index} {...otherConf}></Route>;
  }
});

export const createRouter = (type)=>(params)=>{
  if(type==='client'){
    const history = createBrowserHistory();
    return <Router history={history}>
      <Switch>
        {routes}
      </Switch>
    </Router>
  }else if(type==='server'){
    const {location} = params;
    return <StaticRouter {...params}>
       <Switch>
        {routes}
      </Switch>
    </StaticRouter>
  }
}

客户端

createRouter('client')()

服务端

const context = {};
createRouter('server')({location:req.url,context})  //req.url来自node服务

这里给的只是单层路由的实现,在实际项目中我们会更多的使用嵌套路由,但是二者原理是一样的,嵌套路由的话,小伙伴私下可以实现一下哦!

重定向问题

问题描述

上面讲完前后端路由同构之后,我们发现一个小问题,虽然当url为“/”时,路由重定向到了“/user”了,但是我们打开控制台会发现,返回的内容跟浏览器显示的内容是不一致的。从而我们可以得出这个重定向应该是客户端的路由帮我们做的重定向,但是这样是有问题的,我们想要的应该是要有两个请求,一个请求响应的状态码为302,告诉浏览器重定向到“/user”,另一个是浏览器请求“/user”下的资源。因此我们在服务端我们需要做个改造。

改造代码

import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {createRouter} from '@/router';

const app = express();
app.use(express.static("dist"))
app.get('*',function(req,res){
  const context = {};
  const content = renderToString(<div>
    {createRouter('server')({
        location:req.url,
        context
    })}
  </div>);
  /**
   * ------重点开始
   */
  //当Redirect被使用时,context.url将包含重新向的地址
  if(context.url){
    //302
    res.redirect(context.url);
  }else{
    //200
    res.send(`
        <!doctype html>
        <html>
            <title>ssr</title>
            <body>
                <div id="root">${content}</div>
                <script data-original="/client/index.js"></script>
            </body>
        </html>
    `);
  }
   /**
   * ------重点结束
   */
});
app.listen(3000);

这里我们只加了一层判断,检查context.url是否存在,存在则重定向到context.url,反之则正常渲染。至于为什么这么判断,因为这是react-router官方文档提供的判断是否有重定向的方式,有兴趣的小伙伴可以看一下文档以及源码。文档地址如下:

https://reacttraining.com/rea...

404问题

虽然我在routeConf.js中配置了404页面,但是会有一问题,我们来看一下这张图。

虽然页面正常显示404,但是状态码却是200,这显然是不符合我们的要求的,因此我们需要改造一下。这里我的思路是借助StaticRouter的context,context里面我会放一个常量NOT_FOUND来代表是否需要设置状态码404,放置的时机我选择在Route的render方法里面去设置,具体代码如下。

服务端

import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {createRouter} from '@/router';

const app = express();
app.use(express.static("dist"))
app.get('*',function(req,res){
  const context = {};
  const content = renderToString(<div>
    {createRouter('server')({
        location:req.url,
        context
    })}
  </div>);
  //当Redirect被使用时,context.url将包含重新向的地址
  if(context.url){
    //302
    res.redirect(context.url);
  }else{
    if(context.NOT_FOUND) res.status(404);//判断是否设置状态码为404
    res.send(`
        <!doctype html>
        <html>
            <title>ssr</title>
            <body>
                <div id="root">${content}</div>
                <script data-original="/client/index.js"></script>
            </body>
        </html>
    `);
  }
});
app.listen(3000);

主要就是加了这行代码,通过context.NOT_FOUND是否为true来选择是否设置状态码为404。

 if(context.NOT_FOUND) res.status(404);//判断是否设置状态码为404

routeConf.js

import Login from '@/pages/login';
import User from '@/pages/user';
import NotFound from '@/pages/notFound';


export default [{
  type:'redirect',
  exact:true,
  from:'/',
  to:'/user'
},{
  type:'route',
  path:'/user',
  exact:true,
  component:User,
  loadData:User.loadData
},{
  type:'route',
  path:'/login',
  exact:true,
  component:Login
},{
  type:'route',
  path:'*',
  //将component替换成render
  render:({staticContext})=>{
    if (staticContext) staticContext.NOT_FOUND = true;
    return <NotFound/>
  }
}]

这里我没有选择直接在NotFound组件constructor生命周期上修改,主要考虑这个404组件日后可能会给其他客户端渲染的项目用,尽量保持组件的通用性。

//改造前
component:NotFound
//改造
render:({staticContext})=>{
    if (staticContext) staticContext.NOT_FOUND = true;
    return <NotFound/>
}

加入redux

第一次接触服务端渲染的小伙伴们可能会不太理解这里为什么要单独讲一下如何将redux集成到服务端渲染的项目中去。因为前面在讲原理的时候,我们已经很清楚服务端的作用了,那就是根据访问路径生成对应的html,而redux是javaScript的状态容器,服务端渲染生成html也不需要这玩意儿啊,说白了就是客户端的东西,按照客户端的方式集成不就完了,干嘛还非得单独拎出来讲一下?

这里我就要解释一下了,以掘金的文章详情页为例,假设掘金是服务端渲染的项目,那么每一篇文章的网页源代码里面应该是要包含该文章的完整内容的,这也就意味着接口请求是在服务端渲染html之前就去请求的,而不是在客户端接管页面之后再去请求的,所以服务端拿到请求数据之后得找个地方以供后续的html渲染使用。我们先看看客户端拿到请求数据是如何存储的,一般来说无外乎这两种方式,一个就是组件的state,另一个就是redux。再回到服务端渲染,这两种方式我们一个个看一下,首先是放在组件的state里面,这种方式显然不可取的,都还没有renderToString呢,哪来的state,所以只剩下第二条路redux,redux的createStore第二参数就是用于传入初始化state的,我们可以通过这个方法实现数据注入,大致的流程图如下。

基本框架

首先先展示一下基本目录结构:

页面user下面一个redux文件夹,里面有这三个文件:

  • actions.js (集合redux-thunk将dispatch封装到一个个函数里面,解决action类型的记忆问题)
  • actionTypes.js (reducer所需用的action的类型)
  • reducer.js (常规reducer文件)

有人可能会喜欢以下这种结构,把redux相关的东西放到一个文件夹里,然后分几个大类。但是我个人比较推崇将redux的东西随页面放在一起,这样做的好处就是找起来比较方便,易于维护。可以根据个人喜好来,我只是提供一种我的方式。

actions.js

import {CHANGE_USERS} from './actionTypes';
export const changeUsers = (users)=>(dispatch,getState)=>{
  dispatch({
    type:CHANGE_USERS,
    payload:users
  });
}

actionTypes.js

export const CHANGE_USERS = 'CHANGE_USERS';

reducer.js

import {CHANGE_USERS} from  './actionTypes';
const initialState = {
  users:[]
}
export default (state = initialState, action)=>{
  const {type,payload} = action;
  switch (type) {
    case CHANGE_USERS:
      return {
        ...state,
        users:payload
      }
    default:
      return state;
  }
}

/store/index.js
这个文件的作用就是对多有reducer做一个整合,向外暴露创建store的方法。

import { createStore, applyMiddleware,combineReducers } from 'redux';
import thunk from 'redux-thunk';
import user from '@/pages/user/redux/reducer';

const rootReducer = combineReducers({
  user
});
export default () => {
  return createStore(rootReducer,applyMiddleware(thunk))
};

至于为啥不直接暴露store,原因很简单。主要原因是由于这是单例模式,如果服务端对这个store的数据做了修改,那么这个修改将会一直被保留下来。简单来说a用户的访问会对b用户的访问产生影响,所以直接暴露store是不可取的。

数据的获取以及注入

路由改造

routeConf.js

export default [{
  type:'redirect',
  exact:true,
  from:'/',
  to:'/user'
},{
  type:'route',
  path:'/user',
  exact:true,
  component:User,
  loadData:User.loadData //服务端获取数据的函数
},{
  type:'route',
  path:'/login',
  exact:true,
  component:Login
},{
  type:'route',
  path:'*',
  component:NotFound
}]

首先我们要对前面提到的routeConf.js进行改造,改造的方式很简单,就是加上一个loadData方法,提供给服务端获取数据,至于User.loadData方法的具体实现我们放到后面再讲。看到这里或许有小伙伴会有这样的疑问,通过component就可以拿到User组件,拿到User组件不就可以拿到loadData方法了吗?为啥还要单独配一个loadData呢?针对这个疑问,我在这里做一下说明,就拿User为例,首先你不一定会用component也有可能会用render,其次component对应组件未必就是User,有可能会用类似react-loadable这样的库对User进行一个包装从而形成一个异步加载组件,这样就无法通过component去拿到User.loadData方法了。鉴于component的不确定性,保险起见还是单独配一个loadData更为稳妥一些。

页面改造

在路由改造中,我们提到了需要加一个loadData方法,接下来我们就实现一下。

import React from 'react';
import {Link} from 'react-router-dom';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import axios from 'axios';
import * as actions from './redux/actions';
@connect(
  state=>state.user,
  dispatch=>bindActionCreators(actions,dispatch)
)
class User extends React.PureComponent{
  static loadData=(store)=>{
    //axios本身就是基于Promise封装的,因此axios.get()返回的就是一个Promise对象
    return  axios.get('http://localhost:3000/api/users').then((response)=>{
      const {data} = response;
      const {changeUsers} = bindActionCreators(actions,store.dispatch);
      changeUsers(data);
    });
  }
  render(){
    const {users} = this.props;
    return <div>
        <table>
           <thead>
              <tr>
                <th>姓名</th>
                <th>身高</th>
                <th>生日</th>
              </tr>
           </thead>
           <tbody>
             {
                users.map((user)=>{
                  const {name,birthday,height} = user;
                  return <tr key={name}>
                    <td>{name}</td>
                    <td>{birthday}</td>
                    <td>{height}</td>
                  </tr>
                })
             }
           </tbody>
        </table>
    </div>
  }
}

export default User;

render部分很简单,单纯地显示一个表格,表格有三列分别为姓名、身高、生日这三列。其中表格的数据来源于props的users,当通过接口获取到数据后通过changeUsers方法来修改props的users的值(这个说法其实不准确,users实际来源于store,然后通过connect方法注入到porps中,为了方便那么理解姑且这么说)。整个页面的主逻辑大致就是这样,接下来我们着重讲一下loadData需要注意的地方。

loadData必须有一个参数接受store,返回必须是一个Promise对象

必须要有一个参数接受store,这个比较好理解,根据前面画的流程图,我们是需要修改store里面的state的值的,没有store,修改state值就无从谈起。返回Promise对象主要因为javascript是异步的,但是我们需要等待数据请求完毕之后才能渲染react组件去生成html。当然这里选择callbak也可以实现,但是不推荐,Promise在处理异步上要比callback好太多了,网上有很多文章做了Promise和callback的对比,有兴趣的小伙伴们可以自行查阅,这里就不讨论了。除了Promise比callback在处理异步上更好之外,最主要的一点,同时也是callback的致命伤,就是在嵌套路由的情况下,我们需要调用多个组件的loadData,比如说下面这个例子。

import React from 'react';
import {Switch,Route} from 'react-router';
import Header from '@/component/header';
import Footer from '@/component/footer';
import User from '@/pages/User';
import Login from '@/pages/Login';
class App extends React.PureComponent{
    static loadData = ()=>{
        //请求数据
    }
    render(){
        const {menus,data} = this.props;
        return <div>
            <Header menus={menus}/>
                <Switch>
                   <Route path="/user" exact component={User}/>
                   <Route path="/login" exact component={Login}/>
                </Switch>
            <Footer data={data}/>
        </div>
    }
}

当路径为/user时,我们不仅要调用User.loadData,还要调用App.loadData。如果是Promise,我们可以利用Promise.all来轻松解决多个异步任务的完成响应问题,相反用callback则变得非常复杂。

必须有store.dispatch这个步骤

这点其实也比较好理解,修改store的state的值只能通过调用store.dispatch来完成。但是这里你可以直接调用store.dispatch,或者像我集合第三方的redux中间件来实现,我这里用的redux-thunk,这里我将store.dispatch的操作封装在changeUsers中去了。

import {CHANGE_USERS} from './actionTypes';
export const changeUsers = (users)=>(dispatch,getState)=>{
  dispatch({
    type:CHANGE_USERS,
    payload:users
  });
}

服务端改造

import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {Provider} from 'react-redux';
import {createRouter,routeConfs} from '@/router';
import { matchPath } from "react-router-dom";
import getStore from '@/store';
const app = express();
app.use(express.static("dist"))
app.get('/api/users',function(req,res){
  res.send([{
    "name":"吉泽明步",
    "birthday":"1984-03-03",
    "height":"161"
  },{
    "name":"大桥未久",
    "birthday":"1987-12-24",
    "height":"158"
  },{
    "name":"香澄优",
    "birthday":"1988-08-04",
    "height":"158"
  },{
    "name":"爱音麻里亚",
    "birthday":"1996-02-22",
    "height":"165"
  }]);
});
app.get('*',function(req,res){
  const context = {};
  const store =  getStore();
  const promises = [];
  routeConfs.forEach((route)=> {
    const match = matchPath(req.path, route);
    if(match&&route.loadData){
      promises.push(route.loadData(store));
    };
  });
  Promise.all(promises).then(()=>{
    const content = renderToString(<Provider store={store}>
      {createRouter('server')({
          location:req.url,
          context
      })}
    </Provider>);
    if(context.url){
      res.redirect(context.url);
    }else{
      res.send(`
            <html>
                <head>
                    <title>ssr</title>
                    <script>
                        window.INITIAL_STATE = ${JSON.stringify(store.getState())}
                    </script>
                </head>
                <body>
                    <div id="root">${content}</div>
                    <script data-original="/client/index.js"></script>
                </body>
            </html>
        `);
    }
  });
});
app.listen(3000);

ps:这边加了一个接口“/api/users”是为了后续演示用的,不在改造范围内

集合上面的代码,我们来说一下需要改动哪几点:

根据路径找到所有需要调用的loadData方法,接着传人store调用去获取Promise对象,然后加入到promises数组中。

这里使用的是react-router-dom提供的matchPath方法,因为这里是单级路由,matchPath方法足够了。但是如果多级路由的话,可以使用react-router-config这个包,具体使用方法我就不赘述了,官方文档介绍的肯定比我介绍的更加详细全面。另外有些小伙伴们的路由配置规则可能跟官方提供的不一样,就比如我自己在公司项目中的路由配置方式就跟官方的不一样,对于这种情况就需要自己写一个匹配函数了,不过也简单,一个递归的应用而已,相信难不倒大家。

加入Promise.all,将渲染react组件生成html等操作放入其then中。

前面我们讲过在多级路由中,会存在需要调用多个loadData的情况,用Promise.all可以非常好地解决多个异步任务完成响应的问题。

在html中加入一个script,使新的state挂载到window对象上。

根据流程图,客户端创建store的时候,得传入一个初始state,以达到数据注入的效果,这里挂载到window这个全局对象上也是为了方便客户端去获取这个state。

客户端改造

const getStore =  (initialState) => {
  return createStore(rootReducer,initialState,applyMiddleware(thunk))
};
const store =  getStore(window.INITIAL_STATE);

客户端的改造相对简单了很多,主要就两点:

  • getStore支持传入一个初始state。
  • 调用getStore,并传入window.INITIAL_STATE,从而获得一个注入数据的store。

最终效果图

node层加入接口代理

在讲“加入redux”这个模块的时候,用到了一个“/api/users”接口,这个接口在写在当前服务上的,但是在实际项目中,我们更多地会是调用其他服务的接口。此外除了在服务端调用接口,客户端同样也有需要调用接口,而客户端调用接口就要面临着跨域的问题。因此在node层加入接口代理,不光可以实现多个服务的调用,也能解决跨域问题,一举两得。接口代理我选用http-proxy-middleware这个包,它可以作为express的中间件使用,具体的使用可以查看官方文档了,我这里就不赘述了,我就给个示例供大家参考一下。

准备工作

在使用http-proxy-middleware之前,先做点准备工作,目前“/api/users”是在当前服务上,为了方便演示,我先搭了一个基于json-server的简单mock服务,端口选用了8000,与当前服务的3000端口做一下区分。最后我们访问 “http://localhost:8000/api/users” 可以获取我们想要的数据,效果如下。

开始配置

操作很简单,就是在我们服务端加入一行代码就可以了。

import proxy from 'http-proxy-middleware';
app.use('/api',proxy({target: 'http://localhost:8000', changeOrigin: true }))

如果是多个服务的话,可以这么做。

import proxy from 'http-proxy-middleware';
const findProxyTarget = (path)=>{
  console.log(path.split('/'));
  switch(path.split('/')[1]){
    case 'a':
      return 'http://localhost:8000';
    case 'b':
      return 'http://localhost:8001';
    default:
      return "http://localhost:8002"
  }
}
app.use('/api',function(req,res,next){
  proxy({
    target:findProxyTarget(req.path),
    pathRewrite:{
      "^/api/a":"/api",
      "^/api/b":"/api"
    },
    changeOrigin: true })(req,res,next);
})

不同的服务可以用不同的前缀来区分,通过这个额外的前缀就可以得出对应的target,另外记得用pathRewrite把额外的前缀给去掉,否则就会出现代理错误。

处理css样式

解决报错

  module:{
    rules:[{
      test:/\.css$/,
      use: [
        'style-loader',
        {
            loader: 'css-loader',
            options: {
              modules: true
            }
      }]
    }]
  }

上面这段webpack配置相信大家并不陌生,是用来处理css样式的,但是这段配置要是放在服务端的webpack配置便出现下面这个问题。


从上图的报错来看,提示window未定义,因为是服务端是node环境,因此也就没有window对象。解决问题的办法也很简单,因为从图可知这个报错是来自style-loader的,只要style-loader替换掉即可,替换成isomorphic-style-loader即可。如是你要处理scss、less等等这些文件的话,方法也是一样的,替换style-loader为isomorphic-style-loader即可。

  module:{
    rules:[{
      test:/\.css$/,
      use: [
        'isomorphic-style-loader',
        {
            loader: 'css-loader',
            options: {
              modules: true
            }
      }]
    }]
  }

潜在问题

问题描述


从上面两张图,我们发现一个问题,控制台我们是可以看到style标签的,但是网页源代码却没有,这也就意味这个style标签是js帮我们生成的。这样不好的一点就是页面会闪一下,用户体验不是很好,希望网页源代码可以有这段css,如何实现呢?下面我们就来讲一下。

解决办法

isomorphic-style-loader的官方文档上提供了这个方法,大家可以去isomorphic-style-loader的官方文档查阅一下。这里我说一下需要注意的点。

webpack

webpack配置需要注意两点:

  1. webpack.client.conf.js(客户端)和webpack.server.conf.js(服务端)style-loader必须替换成isomorphic-style-loader。
  2. css-loader必须开启CSS Modules,及其options.modules=true。

组件

import withStyles from 'isomorphic-style-loader/withStyles';
import style from './style.css';
export default withStyles(style)(User);

操作步骤:

  1. 引入withStyles方法。
  2. 引入css文件。
  3. 用withStyles包裹需要导出的组件。

服务端

import StyleContext from 'isomorphic-style-loader/StyleContext';

app.get('*',function(req,res){
    const css = new Set()
    const insertCss = (...styles) => styles.forEach(style => css.add(style._getCss()))
    const content = renderToString(
        <StyleContext.Provider value={{ insertCss }}>
          <Provider store={store}>
              {createRouter('server')({
                  location:req.url,
                  context
              })}
          </Provider>
        </StyleContext.Provider>
   );
   res.send(`
        <!doctype html>
        <html>
          <head>
                <title>ssr</title>
                <style>${[...css].join('')}</style>
                <script>
                    window.INITIAL_STATE = ${JSON.stringify(store.getState())}
                </script>
          </head>
          <body>
                <div id="root">${content}</div>
                <script data-original="/client/index.js"></script>
          </body>
        </html>
     `);        
})

操作步骤:

  1. 引入StyleContext。
  2. 新建Set对象css(为了保证唯一性这里选用Set)
  3. 定义一个insertCss方法,内部逻辑很简单,调用每一个style对象的_getCss方法获取css内容并加到之前定义的Set对象css中去。
  4. 加入StyleContext.Provider,其value是一个对象,该对象里面有一个insertCss属性,该属性对应的值就是前面定义的insertCss方法。
  5. 在渲染的模板中加入style标签,其内容为[...css].join('')

客户端

import React from 'react';
import {hydrate} from 'react-dom';
import StyleContext from 'isomorphic-style-loader/StyleContext'
import App from './app';
const insertCss = (...styles) => {
  const removeCss = styles.map(style => style._insertCss())
  return () => removeCss.forEach(dispose => dispose())
}
hydrate(
  <StyleContext.Provider value={{ insertCss }}>
    <App />
  </StyleContext.Provider>,
  document.getElementById("root")
);

操作步骤:

  1. 引入StyleContext
  2. 定义insertCss方法,内部逻辑为先拿到传入的每一个style对象中_insertCss执行过后的返回值,最后返回一个函数,该函数作用就是将前面拿到返回值再每一个执行一遍。
  3. 加入StyleContext.Provider,其value是一个对象,该对象里面有一个insertCss属性,该属性对应的值就是前面定义的insertCss方法

最终效果:

react-helmet

说到seo优化,有一点大家一定可以答上来,那就是在head标签里加入title标签以及两个meta标签(keywords、description)。在单页面应用中,title和meta都是固定的,但是在多页应用中,不同页面的title和meta可能是不一样的,因此服务端渲染项目是需要支持title和meta的修改的。react生态圈已经有人做了一个库来帮助我们去实现这个功能,这个库就是react-helmet。这里我们说一下其基本使用,更多使用方法大家可以查看其官方文档。

组件

import {Helmet} from "react-helmet";
class User extends React.PureComponent{
    render(){
        const {users} = this.props;
        return <div>
          <Helmet>
            <title>用户页</title>
            <meta name="keywords" content="user" />
            <meta name="description" content={users.map(user=>user.name).join(',')} />
          </Helmet>
        </div>
    }
}

操作步骤:

  1. 在render方法中入一个React元素Helmet。
  2. Helmet内部加入title、meta等数据。

服务端

import {Helmet} from "react-helmet";
app.get('*',function(req,res){
    const content = renderToString(
        <StyleContext.Provider value={{ insertCss }}>
          <Provider store={store}>
              {createRouter('server')({
                  location:req.url,
                  context
              })}
          </Provider>
        </StyleContext.Provider>
    );
    const helmet = Helmet.renderStatic();
    res.send(`
        <!doctype html>
        <html>
          <head>
            ${helmet.title.toString()} 
            ${helmet.meta.toString()}
            <style>${[...css].join('')}</style>
            <script>
              window.INITIAL_STATE = ${JSON.stringify(store.getState())}
            </script>
          </head>
          <body>
            <div id="root">${content}</div>
            <script data-original="/client/index.js"></script>
          </body>
        </html>
  `);
})

操作步骤:

  1. 执行Helmet.renderStatic()获取title、meta等数据。
  2. 将数据绑定到html模版上。
注意事项:Helmet.renderStatic一定要在renderToString方法之后调用,否则无法获取到数据。

结语

看完本篇文章你需要掌握以下内容:

  • 服务端渲染的基本流程。
  • react同构概念。
  • 如何添加路由?
  • 如何解决重定向和404问题?
  • 如何添加redux?
  • 如何基于redux完成数据的脱水和注水?
  • node层如何配置代理?
  • 如何实现网页源代码中有css样式?
  • react-helmet的使用

关于服务端渲染,网上也有不少声音觉得这个东西非常的鸡肋。服务端渲染具有两大优势:

  • 良好的SEO
  • 较短的白屏时间

认为这两个优势根本不算优势,首先是第一个优势,单页应用可以可以通过prerender技术来解决其seo问题,具体实现就是在webpack配置文件中加一个prerender-spa-plugin插件。其次是首屏渲染时间,服务端渲染由于是需要提前加载数据的,这里的白屏时间就需要加上数据等待时间,如果遇到等待时间较长的接口,这体验绝对是不如客户端渲染。另外客户端渲染有很多措施可以减少白屏时间,比如异步组件、js拆分、cdn等等技术。这一来二去“较短的白屏时间”这个优势就不复存在了。我个人认为服务端渲染还是有应用场景的,就比如那种活动页项目。这种活动页项目是非常在意白屏时间,越短的白屏时间越能留住用户去浏览,活动页数据请求很少,也就是说服务端渲染的数据等待时间基本上可以忽略不计的,其次活动页项目的特点就是数量比较多,当数量多到一定程度之后,客户端渲染那些减少白屏时间的手段的效果就不那么明显了。总结一下什么样的项目适合服务端渲染,这个项目要具有以下两个条件:

  • 比较在意白屏时间
  • 接口的等待时间较短
  • 页面数量很多,而且未来有很大的增长空间

个人认为SEO不能作为选择服务端渲染的首要原因,首先服务端渲染项目要比客户端渲染项目复杂不少,其次客户端有技术可以解决SEO问题,再者服务端渲染所带来的SEO提升并不是很明显,想要SEO好,花钱是少不了的。说出来你可能不信,百度搜索“外卖”关键词,第一页居然没有美团,前三条的广告既没有饿了么也没有美团。

在实际项目中还是建议大家使用Next.js这种服务端渲染的框架,Next.js已经非常完善了,简单易用,又有良好的文档,而且还有中文版本哦!这可是不太爱看英文文档的小伙伴们的福音。不太建议大家手动搭一个服务端渲染项目,服务端渲染相对来说比较复杂,未必能面面俱到,本篇文章也只是讲了一些比较核心的点,很多细节点还是没有涉及到的。其次,项目交接给别人也比较费时间。当然,如果是为了更深入地了解服务端渲染,自己手动搭一个最好不过了。最后附上代码地址:

查看原文

赞 56 收藏 38 评论 5

2FPS 赞了文章 · 2019-07-29

css加载会造成阻塞吗

本文由云+社区发表

作者:嘿嘿嘿

可能大家都知道,js执行会阻塞DOM树的解析和渲染,那么css加载会阻塞DOM树的解析和渲染吗?接下来,我就来对css加载对DOM树的解析和渲染的影响做一个测试。

为了完成本次测试,先来科普一下,如何利用chrome来设置下载速度

1. 打开chrome控制台(按下F12),可以看到下图,重点在我画红圈的地方

img

点击我画红圈的地方(No throttling),会看到下图,我们选择GPRS这个选项

2. 点击我画红圈的地方(No throttling),会看到下图,我们选择GPRS这个选项

img

这样,我们对资源的下载速度上限就会被限制成20kb/s,好,那接下来就进入我们的正题

3. 这样,我们对资源的下载速度上限就会被限制成20kb/s,好,那接下来就进入我们的正题

css加载会阻塞DOM树的解析渲染吗?

用代码说话:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>css阻塞</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      h1 {
        color: red !important
      }
    </style>
    <script>
      function h () {
        console.log(document.querySelectorAll('h1'))
      }
      setTimeout(h, 0)
    </script>
    <link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet">
  </head>
  <body>
    <h1>这是红色的</h1>
  </body>
</html>

假设: css加载会阻塞DOM树解析和渲染

假设结果: 在bootstrap.css还没加载完之前,下面的内容不会被解析渲染,那么我们一开始看到的应该是白屏,h1不会显示出来。并且此时console.log的结果应该是一个空数组。

实际结果:如下图

img

css会阻塞DOM树解析?

由上图我们可以看到,当css还没加载完成的时候,h1并没有显示,但是此时控制台输出如下

img

可以得知,此时DOM树至少已经解析完成到了h1那里,而此时css还没加载完成,也就说明,css并不会阻塞DOM树的解析。

css加载会阻塞DOM树渲染?

由上图,我们也可以看到,当css还没加载出来的时候,页面显示白屏,直到css加载完成之后,红色字体才显示出来,也就是说,下面的内容虽然解析了,但是并没有被渲染出来。所以,css加载会阻塞DOM树渲染。

个人对这种机制的评价

其实我觉得,这可能也是浏览器的一种优化机制。因为你加载css的时候,可能会修改下面DOM节点的样式,如果css加载不阻塞DOM树渲染的话,那么当css加载完之后,DOM树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。所以我干脆就先把DOM树的结构先解析完,把可以做的工作做完,然后等你css加载完之后,在根据最终的样式来渲染DOM树,这种做法性能方面确实会比较好一点。

css加载会阻塞js运行吗?

由上面的推论,我们可以得出,css加载不会阻塞DOM树解析,但是会阻塞DOM树渲染。那么,css加载会不会阻塞js执行呢?

同样,通过代码来验证.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>css阻塞</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script>
      console.log('before css')
      var startDate = new Date()
    </script>
    <link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet">
  </head>
  <body>
    <h1>这是红色的</h1>
    <script>
      var endDate = new Date()
      console.log('after css')
      console.log('经过了' + (endDate -startDate) + 'ms')
    </script>
  </body>
</html>

假设: css加载会阻塞后面的js运行

预期结果: 在link后面的js代码,应该要在css加载完成后才会运行

实际结果:

img

由上图我们可以看出,位于css加载语句前的那个js代码先执行了,但是位于css加载语句后面的代码迟迟没有执行,直到css加载完成后,它才执行。这也就说明了,css加载会阻塞后面的js语句的执行。详细结果看下图(css加载用了5600+ms):

img

结论

由上所述,我们可以得出以下结论:

  1. css加载不会阻塞DOM树的解析
  2. css加载会阻塞DOM树的渲染
  3. css加载会阻塞后面js语句的执行、

因此,为了避免让用户看到长时间的白屏时间,我们应该尽可能的提高css加载速度,比如可以使用以下几种方法:

  1. 使用CDN(因为CDN会根据你的网络状况,替你挑选最近的一个具有缓存内容的节点为你提供资源,因此可以减少加载时间)
  2. 对css进行压缩(可以用很多打包工具,比如webpack,gulp等,也可以通过开启gzip压缩)
  3. 合理的使用缓存(设置cache-control,expires,以及E-tag都是不错的,不过要注意一个问题,就是文件更新后,你要避免缓存而带来的影响。其中一个解决防范是在文件名字后面加一个版本号)
  4. 减少http请求数,将多个css文件合并,或者是干脆直接写成内联样式(内联样式的一个缺点就是不能缓存)

原理解析

那么为什么会出现上面的现象呢?我们从浏览器的渲染过程来解析下。

不用浏览器使用的内核不同,所以他们的渲染过程也是不一样的。目前主要有两个:

webkit渲染过程

img

Gecko渲染过程

img

从上面两个流程图我们可以看出来,浏览器渲染的流程如下:

  1. HTML解析文件,生成DOM Tree,解析CSS文件生成CSSOM Tree
  2. 将Dom Tree和CSSOM Tree结合,生成Render Tree(渲染树)
  3. 根据Render Tree渲染绘制,将像素渲染到屏幕上。

从流程我们可以看出来

  1. DOM解析和CSS解析是两个并行的进程,所以这也解释了为什么CSS加载不会阻塞DOM的解析。
  2. 然而,由于Render Tree是依赖于DOM Tree和CSSOM Tree的,所以他必须等待到CSSOM Tree构建完成,也就是CSS资源加载完成(或者CSS资源加载失败)后,才能开始渲染。因此,CSS加载是会阻塞Dom的渲染的。
  3. 由于js可能会操作之前的Dom节点和css样式,因此浏览器会维持html中css和js的顺序。因此,样式表会在后面的js执行前先加载执行完毕。所以css会阻塞后面js的执行。

DOMContentLoaded

对于浏览器来说,页面加载主要有两个事件,一个是DOMContentLoaded,另一个是onLoad。而onLoad没什么好说的,就是等待页面的所有资源都加载完成才会触发,这些资源包括css、js、图片视频等。

而DOMContentLoaded,顾名思义,就是当页面的内容解析完成后,则触发该事件。那么,正如我们上面讨论过的,css会阻塞Dom渲染和js执行,而js会阻塞Dom解析。那么我们可以做出这样的假设

  1. 当页面只存在css,或者js都在css前面,那么DomContentLoaded不需要等到css加载完毕。
  2. 当页面里同时存在css和js,并且js在css后面的时候,DomContentLoaded必须等到css和js都加载完毕才触发。

我们先对第一种情况做测试:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>css阻塞</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script>
      document.addEventListener('DOMContentLoaded', function() {
        console.log('DOMContentLoaded');
      })
    </script>
    <link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet">
  </head>
  <body>
  </body>
</html>

实验结果如下图:

img

从动图我们可以看出来,css还未加载完,就已经触发了DOMContentLoaded事件了。因为css后面没有任何js代码。

接下来我们对第二种情况做测试,很简单,就在css后面加一行代码就行了

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>css阻塞</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script>
      document.addEventListener('DOMContentLoaded', function() {
        console.log('DOMContentLoaded');
      })
    </script>
    <link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet">

    <script>
      console.log('到我了没');
    </script>
  </head>
  <body>
  </body>
</html>

实验结果如下图:

img

我们可以看到,只有在css加载完成后,才会触发DOMContentLoaded事件。因此,我们可以得出结论:

  1. 如果页面中同时存在css和js,并且存在js在css后面,则DOMContentLoaded事件会在css加载完后才执行。
  2. 其他情况下,DOMContentLoaded都不会等待css加载,并且DOMContentLoaded事件也不会等待图片、视频等其他资源加载。

以上,就是所有内容。欢迎关注我们的专栏,接收最新最有趣的前端内容。

此文已由腾讯云+社区在各渠道发布

获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

查看原文

赞 145 收藏 81 评论 16

认证与成就

  • 获得 16 次点赞
  • 获得 27 枚徽章 获得 0 枚金徽章, 获得 4 枚银徽章, 获得 23 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-10-20
个人主页被 999 人浏览