2021年4月

微信截图_20210411191642.png

JS

  • 事件循环模型,同异步,阻塞非阻塞
  • 闭包
  • 作用域链和原型链,原型和继承...
  • 调用栈
  • 复习函数
  • Promise【new】
  • Proxy【new】
  • 数据类型/引用类型
  • 深拷贝和浅拷贝,实现方式的优缺点
  • 柯里化
  • 模块化

Vue

  • Vue2核心理念和Vue3核心理念(比较异同)
  • css中的scoped原理
  • Vdom(这个题react也会问)
  • MVVM(本来不想写)但是确实肯定会考到wi1
  • Vue3带来了什么?有什么新特性
  • 谈谈Vite,为什么这么快,主要解决什么问题?

React

  • useState如何回调?
  • 类组件 vs 函数组件
  • 更新state是同步还是异步的?
  • hooks带来了什么?

Webpack/Vite/SnowPack/Babel

  • 简单讲一下webpack的构建过程
  • webpack有哪些常见的优化手段
  • 常用的loader知道有哪些么?css-loader和style-loader是干嘛的?
  • 常见的plugins有哪些知道么?
  • loader和plugins的区别?
  • 简单讲一下sourceMap
  • 如何提升webpack打包速度呢?
  • webpack的热模块更新怎么做的?
  • webpack模块打包原理?
  • webpack文件监听怎么做的?
  • webpack代码分割本质,有什么意义?
  • 简单聊聊Babel原理吧
  • Vite为什么开发中打包速度那么快?
  • Vite(或者esbuild)为什么ts编译的速度这么快?

CSS

  • Webcomponents规范(那三个核心理念讲出来就好)
  • 如何让页面元素消失?
  • 重排和重绘?如何写组件优化重绘重排带来的开销?(只要答出来contain属性就行了),transform是否引起重排?
  • 如何更改组件内部样式?(深度选择器)
  • css中如何获取html元素上的属性
  • 有了解过calc么?
  • 简单聊聊BFC?如何解决BFC造成的问题?

HTML

  • 有了解过PWA规范么?
  • 简单说一下自定义元素吧(和Webcomponents一起说会更好)
  • 简单说一下可替换元素吧

浏览器和工程化

  • HTTP2带来了什么对于前端?
  • 你如何去架构一个新的前端项目?
  • 浏览器如何优化重排和重绘的?
  • 如何在浏览器中进行标签页面的通信呢?
  • 跨域(其实这题应该不会问了)是怎么造成的
  • 有开发过浏览器插件么?
  • 有了解过微前端么?
  • 有了解过serverless么?
  • 未完待续...

微信截图_20210409201630.png

写在前面

博主要在西安找一个好工作,前端/全栈岗位,中级前端岗位,希望目标公司双休,做自研产品的最好,技术氛围好的优先,水友们有岗位或者HR朋友看到我这个文章请与我联系,po一下简历吧:
沈昊.pdf
联系方式微信:npm_install_s

正文

关于WebComponents的文章其实过年就想写的,但是自身的理解都太片面,所以最近才去边学边写。webCompoents下文中简称WC。从毕业开始如果有面试任务我都会尝试地问一下候选人是否了解过WC,很遗憾面试了半百的人,我连了解过ShadowCompoents的人都没遇到过,可能是面试地薪资要求太低,亦或者是有2-3年开发经验的工程师没有留意过类似的规范,今天我们就来好好梳理一下WC是什么,为什么WC影响了我们现在开发前端的方式吧!

是什么

WC是一套技术,允许开发者创建一套可以定制的元素(组件),相关逻辑和样式都会封装在元素中,并且你可以直接使用它。WC的出现解决了以往前端领域中,对多个具有相似性的功能只能复制粘贴从而造成代码臃肿的问题;WC的出现也推动了模块化/组件化的发展,让更多开发者享受封装组件带来的便利。


WC有3个要素:

  1. Custom Element 自定义元素
  2. ShadowDom 影子盒模型
  3. HTML模板

在HTML中有大多数的标签已经是运用到WC的技术了,比如熟悉的input,video,audio,select等等,我们从现在开始从0实现一个Button组件。要掌握WC的运用,需从实践开始。

影子DOM

我们在写HTML的时候,使用一些标签就可以表达一个具有形式和结构的页面,我们人类去编写这样的代码会很容易,但是机器却不会了,机器需要将HTML转换为真正的文档,而页面结构将会被解析成数据模型(对象/节点),浏览器通过创建这样的节点树(DOM)来确定用户写的HTML的层次结构,DOM最主要的特性是实时的,我们可以通过程序去操控它:

const title = document.createElement("div");
title.textContent = "hello"
document.body.appendChild(title);

这就是为什么我们可以直接通过js来操作页面上的效果就是因为DOM的存在;
那么影子DOM的意思其实已经在字面上了,“隐藏在影子中你看不到”的DOM,举一个简单的例子,我们写一个video标签:

<video>
   <source src="movie.mp4" type="video/mp4">
</video>

控制台显示如下内容:

微信截图_20210408113550.png

在video这样的WC下,有许多DOM都隐藏在其中,这些隐藏DOM都是内置好的功能和样式,在shadowDom中所有的样式和逻辑都是与外部隔绝,不会出现css冲突的问题。

创建一个简单的shadowDom:

const title = document.getElementById("title");
const shadowRoot = title.attachShadow({mode: 'open'});
shadowRoot.innerHTML = "<div>this is shadowDOM</div>"

此shadowRoot存在于title的节点树之下被称之为阴影树,而title就是阴影根,shadowRoot独立于title,shadowRoot中的内容样式逻辑都皆在组件本地,所以这就是shadowRoot不会造成css冲突的原因。

但是需要注意,并不是所有元素都可以承载shadowDOM,以下几种情况shadowDOM将无效/报错:

  1. 已经承载了shadowDOM的元素比如input,textarea等
  2. 元素承载了shadowDOM是img标签

除了我们可以定义”open“的shadowDOM之外,我们还可以定义闭合的shadowDOM:

const shadowRoot = title.attachShadow({mode: 'closed'});

HTML内部的Video标签就是一个闭合的shadowDOM,它的意义主要在于外部的JS是无法访问这个shadowDOM的,无论你使用assignedSlot/composedPath等等都是无效的。请记住闭合的shadowDOM不是很有用处,大可不必使用它,它不是我们理解的“安全的ShadowDOM”。

创建Custom Element

我们创建一个自定义的元素,结合使用shadowDOM,来完成我们开头说的button组件。

customElements.define(
  "i-button",
  class extends HTMLElement {
    constructor() {
      super();
      const shadowRoot = this.attachShadow({ mode: "open" });
      shadowRoot.innerHTML = `
        <div class="button">
            <slot name="icon"></slot>
            <slot></slot>
        </div>
      `;
    }
  }
);

我们通过customElements对象创建一个自定义组件,组件接收一个类,此时i-button的影子DOM就是我们在构造函数中定义的,影子DOM内容是标签,关于插槽稍后再讲述,我们先尝试使用i-button。

<i-button>提交</i-button>

页面正常渲染我们的button组件,如果不出意外的话,你能在控制台看到我们刚刚编写的自定义元素以及元素下的shadowDOM。

组合和插槽

组合在shadowDOM中是一个很重要的概念,我们在HTML中使用各式各样的标签完成页面,页面的构成是各种标签的组合,而组件也是一样,例如video标签,我们通过video的子级source来定义媒介资源地址,但是它却不会渲染。

别着急,我们先来梳理下几个术语概念:

Light DOM

指的是用户编写的内容,比如在上文中,我们使用了i-button组件,在这个组件中我们写下了字 “提交”那么此时“提交”就是Light DOM,此时“提交”是实际子元素,它是真实存在的,而不像是shadowDOM。

Shadow DOM

具体的意义上文提到过,补充一下ShadowDOM对组件而言是本地的,它还可以定义一些“标记”或者说是“插槽

使用过vue的水友们,应该更能体会插槽带来的便利,插槽是组件内部的占位符,方便使用者编写的LightDOM按照指定的方式和组件一起呈现出来,那么这个指定的方式也就是插槽的类型了:

具名插槽

在组件内部定义的之后,我们使用组件的时候可以这么使用:

<i-button>
    <img slot="icon" src="icon.png"></img>
    提交
</i-button>

默认插槽

默认插槽就是我们组件中写的内容,比如就是上文中的提交二字,它没有被slot标记,就会默认放在组件中的位置渲染。如果用户不在组件提供LigntDOM,那么我们可以定义一个后备插槽以便备用:


/* 默认渲染的位置 */
<slot></slot>
<slot>如果用户没传递内容,那么将会显示我</slot>

当用户编写的LightDOM被组件定义的插槽使用了,那么此时,这个元素并不是被插槽移动了位置,插槽没有移动位置的功能,其实就是浏览器把LightDOM元素渲染到了shadowDOM的位置上了而已。

理解了插槽之后,我们就可以使用更多的标签将其组合在一起,构成一个较为完整且实用的组件了,当然还有样式!

样式

ShadowDOM最有趣的特型就是作用域CSS了,外部的css选择器不会影响到shadowDOM,内部的也不会影响外部的,css的作用域为阴影根
我们来定义一下Button组件的样式:

#shadow-root
<style>
 .button{
   background: red;
 }
</style>
<div class="button">
  <slot name="icon"></slot>
  <slot></slot>
</div>

oh, no,尽管它定义成了红色很丑,但是不妨碍我们研究它的作用域CSS;

<link rel="stylesheet" href="styles.css">
<div></div>

还可以加入link标签引入一个css,这个css也是带有作用域的。
我们在写WC的时候,不仅会需要组件自身维护自己的样式,也需要外部组件可以通过一种方法改变组件内部样式,这样既保证了封装性又有灵活性,那么:host这个伪类是需要了解的。

:host是一个选择宿主的伪类选择器,我们可以使用:host来匹配宿主或者宿主下的元素:

<style>
    :host{
        color: #fff;
    }
</style>

也可以匹配阴影根下的元素:

:host(.button){
    background: blue;
}

也可以定义插槽样式:

::slotted(.icon){
   width: 2px;
   height: 2px;
}

从外部定义自定义组件的样式,直接使用元素名进行设置:

i-button:hover{
    opacity: 0.8;
}

当外部设置了样式之后,优先级会大于内部的css规则,比如:

:host{
   opacity: 0.1;
}

通常开发者编写自己的组件的时候,会使用CSS自定义属性,而使用者可以修改其CSS自定义属性:

<style>
    i-button{
        /*我很喜欢红色*/
        --diy-bg: red; 
    }
</style>
<i-button background></i-button>

影子dom这样写:

:host([background]){
    background: var(--diy-bg, black);
}

我们在使用组件的时候给自定义元素设置了一个值为red,然后在自定义元素中加了一个“background”的属性,然后在其shadowDOM中匹配了元素如果有属性background的话:就设置背景为外部传入的“red”,如果外部没有传入,则就是默认的“black“。

当然开发组件的时候,我们需要告知使用者一些内置的css自定义属性。

技巧

使用css containment

我们可以使用“css遏制”来优化web组件重排重绘的性能,当web组件内部进行了UI/位置变更,势必会引起页面的重排和重绘,使用css遏制之后告诉浏览器,组件这一块是一块独立的DOM,浏览器就不会造成整个页面的重排重绘了,只会在组件内部进行重排和重绘。

:host {
  display: block;
  contain: content; /* Boom. CSS containment FTW. */
}

使用Template

使用Template代替innerHTML,使用template之后你会发现,vue的组件就是使用这种方式来呈现组件的,它们都是一样的!使用template会更清晰地看到组件的DOM结构。

<template id="i-button">
    <div class="button">
      <slot name="icon"></slot>
      <slot></slot>
    </div>
</template>
<script>
    customElements.define(
      "i-button",
      class extends HTMLElement {
        constructor() {
          super();
          var template = document
            .getElementById('i-button')
            .content;
          const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(template.cloneNode(true));
          `;
        }
      }
    );
  </script>
    // 使用Button组件
    <i-button></i-button>
    <style>
         // 增加一些Style样式
    </style>

剑指题解


互联网人题解神器,神器在手,offer我有

随手刷题 | 无广告绿色免费 | 个性推荐 | 每日自动推送 | 多维度丰富领域知识

项目的起源是一个 github 仓库,名为前端知识每日 3+1,正是这个优秀的题库项目才有了我们团队开发聚合题库的想法。在我们国内程序员圈子,大多数小伙伴为了更好的技术发展和 offer,都通过训练题来提高自己的眼界,其中不乏包括算法/业务/软技能的题目,但是因为我们的环境和侧重点不一样,所以我们接触的项目和题目都不一样,而市面上很少有一个项目能做到,涵盖不同圈子的聚合题库。 《剑指题解》项目就是一个优秀的互联网行业聚合题库神器。


开源免费不易,请关注我们的公众号以及开源仓库

沈昊Seho/InternetQuestionBank<br/>
Github 点 star, 拉取代码时请选择最新分支
Dcloud 插件市场支持我们

目录

特点

  1. 汇聚优秀的开源题目,以技术社区为主线展开的刷题 APP
  2. 根据用户的喜好,去推送相关的最新题目,不会再刷到 “牛头不对马嘴”无用的题目
  3. 我们绑定了服务号,每日定时推送给用户最新的题目,让你能在每天清晨就开启活力新一天
  4. 刷题 APP 无广告,无硬性推广,一切盈利皆在赞助/官网流量
  5. 我们还有很多有意思的待开发 feature,比如大佬带萌新模式,还将开启校园/企业的通道,让每一个学生/员工都能每时每刻提高自己
  6. 剑指题解开源项目,从原型图到小程序,UI 图,UML,API 文档,第三方 API 对接说明书,使用说明都将免费开源,欢迎各位同僚二开,让这个行业变得更好,我们将用心地辅助你们解决在技术上的难题。

技术栈

关于技术栈的选型可以参考我们的语雀文档
周边物料的开源,还需要感谢 mockPlus 以及语雀:

<img class="imgIcon" src="http://static.yinzhuoei.com/typecho/2021/01/17/69752764374919.png" width="10%">


<img class="imgIcon" src="http://static.yinzhuoei.com/typecho/2021/01/17/697832282748067.jpg" width="20%"></img>

UML

由于此部分在开发新 feature 的工作中可能会进行变更或者本就有实质性的错误,欢迎向我们反馈错误,我们将感激不尽。

类图

拉取任务

更多

更多相关资料请移步
out/doc或者语雀-UML

设计概览



设计源文件请移步:点击查看-为了您的体验,请务必下载最新版本的设计图
原型图在线预览: 点击查看, 如果失效请联系我们

API

《剑指题解》团队在 API 层面,开源的内容非常有限制,因为涉及到多位合作伙伴的题库授权,所以如果您要使用聚合题库 API 请联系我们商议,如果您是学习者,那么在语雀中的 API 文档会帮助到您,在这里我们将简单介绍。

uniapp 的云函数开发,我们是第一次接触,所以在此之前我们做了很多调研,在下文也提到了关于 explain.js 这个框架的作用,我们也是非常感谢 uniapp 开源社区有这样创作质量的第一批的 unicloud 框架开发者,在咨询了官方人员之后,我们了解到如果要使用 ts 来开发,需要把 ts 编译成 js 来放在云函数目录,所以我们选择了以下的技术栈,开发者可以很爽的使用 ts 来开发 unicloud 云函数:

  • esbuild-node-tsc
  • nodemon

esbuild-node-tsc 这个工具是基于 esbuild 的,利用其特性,我们可以比 ts 官方提出的 tsc 编译工具更快,快几十倍到上百倍,这也是得益于 esbuild 的使用 go 语言直接编译的特性。

nodemon 可以帮助我们监听目录文件,一旦改变就执行打包 ts 命令直接将编译好的 js 放在指定目录
所以我们把 explain.js 的配置也相应的改变,我们在 services 目录中存储的是我们 ts 文件,在 dist 中存储的是我们编译好的 js 文件,而 dist 目录就是 explain 需要的:

config.init({
  baseDir: __dirname,
  serviceDir: "/dist/",
});

需要注意的事情是:
由于云函数大小限制,我们需要把相关依赖全部安装到全局,包括 typescript

读写职责分离

读写职责分离模式(CQRS)是一种把查询(Queries) 数据和和更新(Commands) 数据通过使用各自独立的接口分开的模式。
Uniapp 的 Unicloud 很好,其中的 jql 也是大开眼界,确实统一了前端开发操作数据库的体验,让我们前端开发操作数据库非常友好,但是我们使用 CQRS 将这些原本封装的很好的 API 不予以使用。

  • 前端使用 jql 去读取想要的数据
  • 写入操作就交给云函数去执行

如图,前端读取一个列表数据

const db = uniCloud.database();
const databaseName = "testInit";
// 获取测试数据列表
export function getTestList() {
  return new Promise((resolve) => {
    db.collection(databaseName)
      .get()
      .then((res) => {
        // res 为数据库查询结果
        resolve(res);
      })
      .catch((err) => {
        // err.message 错误信息
        // err.code 错误码
      });
  });
}

我们将这样的文件抽出了一个一个查询模块,每一个模块对应了一个数据表(这里是 testInit)封装在了 API 这个文件夹中。

Vue 页面将这样去调用,这样我们在 API 查询层将做好数据的处理,确保 Vue 拿到的是可以直接渲染的干净数据。

import { getTestList } from "../../api/test";
const data = getTestList();

我们写入操作将通过云函数直接调用,唯一设计不同的地方在于,我们将云函数这个概念改变了,应该是模块化,我们把一个一个函数变成了模块。

在我们的初步技术调研过程中,由于 Uni 官方并没有提供给用户云函数开发框架,但是我们在插件市场中找到了一款名为 explain 的开发框架,它可以迅速的帮助我们实现 restapi 风格的单路由云函数,这款框架我们不多做介绍,文档在这里:explain.js 快速开发 uni 云函数的框架

// 注册用户根据手机号
  addUserByPhone() {
    return handleMustRequireParam(
        [{
            key: "username",
            value: "用户名",
          },
          {
            key: "password",
            value: "密码",
          },
        ],
        this.event.params
      )
      .then(async () => {
        const {
          username,
          password
        } = this.event.params;
        if (!/^1[3456789]\d{9}$/.test(username)) {
          return appErrorMessage("用户名格式不正确");
        } else if (password === "" || password.length < 6) {
          return appErrorMessage("密码格式不正确");
        } else {
          // 校验手机号
          return await uniID.register({
            username,
            password,
          });
        }
      })
      .catch((err) => {
        return err;
      });
  }

那么我们如果要调用 testPrint 这个模块中的增加操作

uniCloud.callFunction({
  name: "application",
  data: {
    route: "api/user",
    method: "POST",
    params: {
      username: "18291563764",
      password: "sas",
    },
  },
});

贡献者

感谢为《剑指题解》这个优秀项目贡献自己一份力的小伙伴们:

如果您想加入到我们的贡献者队列中,请联系我们,这里还有相关贡献者的介绍,希望对您有帮助

核心维护者

相关开源物料

题库合作伙伴

QQ 群和钉钉群欢迎大家加入


许可

Apache License © 剑指题解
如果您在其他有关我们的文档见到了与之不符合的协议内容,请联系我们,这可能是我们的历史遗留的代码问题。