图灵深视 前端开发工程师 一面
1. 讲一下微信小程序中登录是怎么实现的
原理概述
- 微信小程序登录主要基于微信官方提供的登录接口。其核心是通过调用微信的登录 API 获取用户的登录凭证(code),然后将这个 code 发送到开发者服务器,服务器再使用这个 code 向微信服务器换取用户的唯一标识(openid)和会话密钥(session_key)。
具体步骤
- 小程序端:使用
wx.login()
方法获取用户登录凭证(code)。例如:
- 小程序端:使用
wx.login({
success: function (res) {
if (res.code) {
// 将code发送到服务器
wx.request({
url: 'https://yourserver.com/login',
data: {
code: res.code
},
method: 'POST',
success: function (response) {
// 处理服务器返回的用户信息等
}
});
} else {
console.log('获取用户登录态失败!' + res.errMsg);
}
}
});
- 服务器端:收到小程序发送的 code 后,向微信服务器发送请求来换取 openid 和 session_key。一般是通过向
https://api.weixin.qq.com/sns/jscode2session
接口发送请求,需要传入小程序的appid
、secret
和收到的code
。示例代码(以 Node.js 为例):
const https = require('https');
const appid = 'YOUR_APPID';
const secret = 'YOUR_SECRET';
function getOpenId(code) {
const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appid}&secret=${secret}&js_code=${code}&grant_type=authorization_code`;
return new Promise((resolve, reject) => {
https.get(url, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const result = JSON.parse(data);
resolve(result.openid);
} catch (e) {
reject(e);
}
});
}).on('error', (err) => {
reject(err);
});
});
}
- 之后,开发者可以根据 openid 等信息来实现自己的业务逻辑,如生成自定义的用户登录态,存储用户信息等。
2. 谈一下你对小程序的理解
定义和特点
- 微信小程序是一种不需要下载安装即可使用的应用。它基于微信生态,具有轻便、快捷的特点。用户可以通过扫描二维码或在微信中搜索等方式直接打开使用。
开发模式
- 小程序采用了类似前端开发的技术栈,主要使用 JavaScript、WXML(类似于 HTML)和 WXSS(类似于 CSS)。它有自己的一套组件库,方便开发者快速构建用户界面。例如,小程序提供了视图容器组件(如
view
)、基础内容组件(如text
)等。
- 小程序采用了类似前端开发的技术栈,主要使用 JavaScript、WXML(类似于 HTML)和 WXSS(类似于 CSS)。它有自己的一套组件库,方便开发者快速构建用户界面。例如,小程序提供了视图容器组件(如
应用场景
- 小程序适用于多种场景,如电商购物场景,用户可以在小程序中浏览商品、下单购买;生活服务场景,像预约挂号、点餐等服务都可以通过小程序实现。其优势在于能够快速触达用户,并且在微信的生态环境下,能够方便地与微信的其他功能(如支付、社交分享)相结合。
3. Echart底层原理你了解吗
数据驱动
- Echarts 是一个数据可视化库,它的核心是数据驱动的可视化设计。它将数据和可视化的配置进行分离。开发者只需要提供数据和对应的可视化配置(如图表类型、坐标轴设置等),Echarts 会根据这些信息来绘制图表。
渲染流程
- 首先,Echarts 会对传入的数据进行解析,根据配置确定图表的基本结构,如坐标轴、图例、图形元素(柱状、折线等)的布局。然后,通过使用底层的渲染引擎(如在浏览器环境下主要使用 HTML5 Canvas 或 SVG)将这些图形元素绘制出来。
- 以使用 Canvas 渲染为例,Echarts 会根据数据计算出每个图形元素的坐标、大小等属性,然后使用 Canvas 的 API(如
drawRect
用于绘制矩形,beginPath
和lineTo
等用于绘制折线等)将图形绘制在 Canvas 上。如果是 SVG 渲染,它会生成相应的 SVG 元素(如<rect>
、<line>
等)来构建图表。
交互原理
- Echarts 支持多种交互功能,如缩放、数据提示等。当用户进行交互操作时,如鼠标移动到图表元素上,Echarts 会根据鼠标的位置和图表的布局等信息,通过事件监听机制(如监听
mouseover
事件)来触发相应的交互行为。例如,显示数据提示框,这个数据提示框的内容是根据当前鼠标位置对应的数据点来动态生成的。
- Echarts 支持多种交互功能,如缩放、数据提示等。当用户进行交互操作时,如鼠标移动到图表元素上,Echarts 会根据鼠标的位置和图表的布局等信息,通过事件监听机制(如监听
4. 图表刷新、点击穿透、回流重构等问题
图表刷新
- 原因和场景:图表需要刷新通常是因为数据发生了变化。例如,在实时数据监控的场景下,数据每隔一段时间就会更新,这就需要刷新图表来展示最新的数据。
- 解决方法:对于 Echarts 等图表库,一般可以通过更新数据并重新设置图表的数据集来实现刷新。以 Echarts 为例,如果是折线图,假设
myChart
是已经初始化的图表实例,newData
是新的数据,可以通过以下方式刷新:
myChart.setOption({
series: [{
data: newData
}]
});
点击穿透
- 定义和问题表现:点击穿透是指当一个元素(如蒙层)覆盖在另一个可点击元素上时,点击蒙层后,下层的元素也触发了点击事件。例如,在一个模态框(蒙层)覆盖在页面内容上,当用户点击模态框关闭按钮以外的区域来关闭模态框时,下层的页面元素的点击事件也被触发了。
- 解决方法:一种常见的解决方法是在蒙层元素上添加
@click.stop
(在 Vue 中)或者event.stopPropagation()
(在原生 JavaScript 中)来阻止事件冒泡。例如在 Vue 中:
<div class="overlay" @click.stop>
<!-- 模态框内容 -->
</div>
回流重构
- 定义和影响:回流是指浏览器重新计算元素的布局位置等属性,重构是指浏览器重新绘制元素的外观。回流的触发场景包括元素的大小、位置、内容等发生变化。频繁的回流和重构会影响性能,导致页面卡顿。
- 优化方法:尽量减少对布局有影响的操作,如避免频繁地改变元素的尺寸和位置。如果需要批量操作元素,可以通过将元素从文档流中脱离(如使用
position:absolute
或display:none
),进行操作后再将其恢复,来减少回流次数。
5. 你简历上谈到了跨域问题处理,你是怎么处理的
跨域产生的原因
- 浏览器的同源策略限制,当一个资源(如一个 JavaScript 文件、一个 API 请求)的协议、域名、端口与当前页面的不一致时,就会产生跨域问题。例如,前端页面是在
http://localhost:8080
访问,而后端 API 是在http://api.example.com
提供服务,就会产生跨域。
- 浏览器的同源策略限制,当一个资源(如一个 JavaScript 文件、一个 API 请求)的协议、域名、端口与当前页面的不一致时,就会产生跨域问题。例如,前端页面是在
解决方法(以 Node.js 后端为例)
- CORS(跨域资源共享):在后端服务器设置响应头来允许跨域访问。在 Node.js 中使用
express
框架可以这样设置:
- CORS(跨域资源共享):在后端服务器设置响应头来允许跨域访问。在 Node.js 中使用
const express = require('express');
const app = express();
app.use((req, res, next) => {
res.header('Access - Control - Allow - Origin', '*');
res.header('Access - Control - Allow - Methods', 'GET, POST, PUT, DELETE');
res.header('Access - Control - Allow - Headers', 'Content - Type, Authorization');
next();
});
- 代理服务器:在开发环境下,前端开发工具(如 Vue - CLI)可以设置代理。以 Vue - CLI 为例,在
vue.config.js
文件中可以这样设置:
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://api.example.com',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
};
6. Vue 中监听和计算属性的区别
计算属性(computed)
- 定义和特点:计算属性是基于它们的依赖进行缓存的。一个计算属性只有在它的依赖数据发生变化时才会重新计算。例如,假设有一个计算属性
fullName
,它依赖于firstName
和lastName
:
- 定义和特点:计算属性是基于它们的依赖进行缓存的。一个计算属性只有在它的依赖数据发生变化时才会重新计算。例如,假设有一个计算属性
const app = Vue.createApp({
data() {
return {
firstName: 'John',
lastName: 'Doe'
};
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName;
}
}
});
- 应用场景:适用于需要对已有数据进行复杂计算并缓存结果的情况。比如根据商品的单价和数量计算总价,总价是一个计算属性,只要单价和数量没有变化,总价就不需要重新计算,直接从缓存中获取。
监听属性(watch)
- 定义和特点:监听属性用于在数据变化时执行异步操作或开销较大的操作。它更侧重于观察数据的变化并作出响应。例如,监听一个表单输入框的值的变化:
const app = Vue.createApp({
data() {
return {
inputValue: ''
};
},
watch: {
inputValue(newValue, oldValue) {
console.log(`输入值从 ${oldValue} 变为 ${newValue}`);
}
}
});
- 应用场景:适用于当数据变化后需要执行一些副作用操作,如发送网络请求、更新其他相关数据等。比如当用户在搜索框输入关键词后,监听关键词的变化并发送请求获取搜索结果。
7. Vue3 和 Vue2 的区别
性能方面
- 响应式系统改进:Vue3 采用了 Proxy 来实现响应式,相比 Vue2 的
Object.defineProperty
,Proxy 可以代理整个对象,而不是像Object.defineProperty
那样只能代理对象的已有属性。这使得在添加或删除属性时也能被响应式地追踪,并且在性能上有一定的提升,特别是在处理大型复杂对象时。
- 响应式系统改进:Vue3 采用了 Proxy 来实现响应式,相比 Vue2 的
API 设计
- 组合式 API(Composition API):Vue3 引入了组合式 API,它允许开发者以函数的形式组织和复用代码逻辑。例如,在 Vue3 中可以使用
setup
函数来组合多个逻辑相关的代码块,包括数据、方法、生命周期钩子等。相比 Vue2 主要基于选项式 API(data
、methods
、mounted
等选项),组合式 API 提供了更灵活的代码组织方式。
- 组合式 API(Composition API):Vue3 引入了组合式 API,它允许开发者以函数的形式组织和复用代码逻辑。例如,在 Vue3 中可以使用
模板语法
- 片段支持:Vue3 支持在模板中有多个根元素(片段),而 Vue2 要求模板必须有且只有一个根元素。例如,在 Vue3 中可以这样写模板:
<template>
<div>第一个元素</div>
<div>第二个元素</div>
</template>
8. Vue 组件间通信
父子组件通信
- 父传子:在父组件中通过属性(
props
)将数据传递给子组件。例如,父组件中有一个数据message
,在子组件中通过props
接收:
- 父传子:在父组件中通过属性(
<!-- 父组件 -->
<template>
<child-component :message="message"></child-component>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
message: 'Hello from parent'
};
}
};
</script>
<!-- 子组件 -->
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
props: ['message']
};
</script>
- 子传父:子组件通过触发自定义事件将数据传递给父组件。例如,子组件中有一个按钮,点击按钮时向父组件传递一个数据:
<!-- 子组件 -->
<template>
<button @click="sendData">发送数据</button>
</template>
<script>
export default {
methods: {
sendData() {
this.$emit('data - from - child', 'Data from child');
}
}
};
</script>
<!-- 父组件 -->
<template>
<child-component @data - from - child="handleData"></child-component>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
methods: {
handleData(data) {
console.log(data);
}
}
};
</script>
非父子组件通信(事件总线或 Vuex)
- 事件总线:在 Vue2 中可以创建一个空的 Vue 实例作为事件总线。例如,定义一个
EventBus.js
文件:
- 事件总线:在 Vue2 中可以创建一个空的 Vue 实例作为事件总线。例如,定义一个
import Vue from 'vue';
export const eventBus = new Vue();
然后在组件 A 中发送事件:
import { eventBus } from './EventBus.js';
eventBus.$emit('custom - event', 'Data to share');
在组件 B 中接收事件:
import { eventBus } from './EventBus.js';
created() {
eventBus.$on('custom - event', (data) => {
console.log(data);
});
}
- Vuex:Vuex 是一个专门用于 Vue 应用的状态管理模式。它通过一个集中式的存储(store)来管理应用的所有组件的状态。组件可以通过
commit
方法触发mutations
来修改状态,通过dispatch
方法触发actions
来执行异步操作并修改状态。例如,在store.js
文件中定义一个简单的 Vuex store:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++;
}
},
actions: {
asyncIncrement(context) {
setTimeout(() => {
context.commit('increment');
}, 1000);
}
},
getters: {
doubleCount(state) {
return state.count * 2;
}
}
});
在组件中使用:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { mapState, mapMutations, mapActions } from 'vuex';
export default {
computed: {
...mapState(['count'])
},
methods: {
...mapMutations(['increment']),
...mapActions(['asyncIncrement'])
}
};
</script>
9. Vue 两种路由以及区别
Hash 模式
- 原理:Hash 模式是基于浏览器的
location.hash
来实现路由的。location.hash
的值就是 URL 中#
后面的部分。当#
后面的值发生变化时,浏览器不会向服务器发送请求,而是会触发hashchange
事件。例如,http://example.com/#/home
中的#/home
就是hash
值。 - 特点:这种模式比较简单,兼容性好,因为它不会引起浏览器的页面刷新。但是
hash
值在 URL 中比较明显,不太美观。
- 原理:Hash 模式是基于浏览器的
History 模式
- 原理:History 模式是利用 HTML5 的
history.pushState()
和history.replaceState()
方法来改变 URL,并且不会引起页面刷新。例如,通过history.pushState({}, '', '/home')
可以将当前 URL 修改为http://example.com/home
。同时,当用户点击浏览器的前进或后退按钮时,会触发popstate
事件,根据这个事件可以实现路由的切换。 - 特点:History 模式的 URL 比较美观,更符合传统的 URL 格式。但是需要服务器端的配合,因为当用户直接访问一个不存在的 URL(如
http://example.com/about
)时,服务器需要返回正确的页面(通常是单页应用的index.html
),否则会出现 404 错误。
- 原理:History 模式是利用 HTML5 的
10. 实现页面元素垂直居中
方法一:使用 Flexbox 布局
- 原理:Flexbox 是一种强大的布局模式,通过设置父元素为
display: flex
,并使用align-items: center
和justify-content: center
属性,可以轻松地将子元素在水平和垂直方向上居中。例如,对于一个简单的 HTML 结构:
- 原理:Flexbox 是一种强大的布局模式,通过设置父元素为
<div class="parent">
<div class="child">
这是要居中的内容
</div>
</div>
CSS 样式如下:
.parent {
display: flex;
justify-content: center;
align-items: center;
height: 300px; /* 假设父元素有一定高度 */
}
方法二:使用 CSS Grid 布局
- 原理:CSS Grid 布局也提供了方便的居中方式。通过将父元素设置为
display: grid
,然后使用place-items: center
属性来同时在水平和垂直方向上居中子元素。例如:
- 原理:CSS Grid 布局也提供了方便的居中方式。通过将父元素设置为
<div class="grid - parent">
<div class="grid - child">
这是要居中的内容
</div>
</div>
CSS 样式:
.grid - parent {
display: grid;
place-items: center;
height: 300px; /* 假设父元素有一定高度 */
}
方法三:绝对定位和负边距
- 原理:对于已知高度和宽度的元素,可以使用绝对定位将其定位到父元素的中心位置。首先将父元素设置为相对定位(
position: relative
),然后子元素使用绝对定位(position: absolute
),并通过设置top
、left
属性为50%
将元素的左上角定位到父元素的中心,最后使用负边距(margin - top
和margin - left
)将元素向回拉回自身高度和宽度的一半,从而实现居中。例如:
- 原理:对于已知高度和宽度的元素,可以使用绝对定位将其定位到父元素的中心位置。首先将父元素设置为相对定位(
<div class="parent - abs">
<div class="child - abs">
这是要居中的内容
</div>
</div>
CSS 样式:
.parent - abs {
position: relative;
height: 300px;
}
.child - abs {
position: absolute;
top: 50%;
left: 50%;
width: 200px;
height: 100px;
margin - top: - 50px;
margin - left: - 100px;
}
方法四:使用
transform
属性结合绝对定位- 原理:与绝对定位和负边距类似,不过这种方法不需要知道元素的具体尺寸。同样先将父元素设置为相对定位,子元素设置为绝对定位,将
top
和left
设置为50%
,然后使用transform: translate(-50%, -50%)
来将元素在水平和垂直方向上移动自身宽度和高度的一半,实现居中。例如:
- 原理:与绝对定位和负边距类似,不过这种方法不需要知道元素的具体尺寸。同样先将父元素设置为相对定位,子元素设置为绝对定位,将
<div class="parent - trans">
<div class="child - trans">
这是要居中的内容
</div>
</div>
CSS 样式:
.parent - trans {
position: relative;
}
.child - trans {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
You are very good.