实习:摸鱼!摸鱼!究极摸鱼!(实习和面试结合)
前言
本文是在实习阶段沉淀下的问题,并对其进行了总结和思考,把面试中会问到和这些问题结合开发场景进行了描述,也算是面试题,但相对于八股文比较贴近实际开发。
项目准备
公司使用仓库是gitlab,入职时发了一个账号和一台电脑
git命令
设置你的用户名和邮箱
$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com
拷贝仓库的代码
git clone 拷贝仓库的代码
拷贝了代码之后你需要查看仓库中有哪些分支
$ git branch 查看本地所有分支
$ git branch -r 查看远程所有分支
$ git branch -a 查看所有的分支,包括本地分支和远程分支。
当你查看好了仓库中的分支,就开开始选择分支进行开发
$ git checkout xxx(分支名) 切换分支
$ git checkout -b xxx(分支名) 建立一个新的本地分支 xxx
开发完后把代码提交到远程仓库
$ git add [file name] 添加一个文件到 git index(暂存区)
$ git commit -m "This is the message describing the commit" 添加 commit 信息
$ git push 将文件给推到服务器上对应的分支
开发中项目负责人在你的分支上添加了一个新的组件,让你拉一下
$ git pull 从远程仓库拉取(获取)最新的代码并合并到当前分支
(这个命令实际上相当于执行了两个操作:`git fetch` 和 `git merge`)
$ git fetch 从远程仓库获取最新的代码、分支和提交,但并不会自动合并到本地分支。
$ git merge 用于将一个分支的更改合并到另一个分支
$ git rebase 用于将一个分支的更改合并到另一个分支
git merge 和 git rebase 的区别
-
git merge
:git merge
用于将一个分支的更改合并到另一个分支。- 它会将指定分支的更改合并到当前分支,并创建一个新的合并提交。
- 合并会保留分支的历史,即每个分支的提交历史都保留在合并后的分支中。
-
git rebase
:git rebase
也用于将一个分支的更改合并到另一个分支,但它的合并方式是“衍合”式的,即将一系列提交应用于目标分支。- 它会将当前分支中的提交“挪动”到目标分支的最新提交之后。
- 通过
git rebase
可以使提交历史更线性、清晰,但可能会改变提交的顺序和哈希值,不保留原始分支的提交历史。
临时空间
$ git status 查看当前状态
$ git stash push 将文件给 push 到一个临时空间中
$ git stash pop 将文件从临时空间 pop 下来
git冲突
当在远程仓库的某个文件发生修改,并且你在本地没有进行 git pull
操作时,如果你在本地也对同一个文件进行了修改,并尝试将这些修改推送到远程仓库,就可能会引发冲突。这是因为远程仓库和您的本地仓库之间存在不同的修改,Git 不知道如何自动合并这些修改,从而导致冲突。
解决这种情况的一种常用方法如下:
-
运行
git stash push
将本地修改暂存起来,以便后续恢复。 -
运行
git pull
从远程仓库拉取更新。 -
运行
git pull
后,如果发生冲突,Git 会将冲突的文件标记为未解决状态,您可以通过运行git status
查看哪些文件发生了冲突。 -
打开冲突文件,您会看到冲突部分用特定标记包围,示例:
plaintextCopy code <<<<<<< HEAD 本地代码修改 ======= 远程代码修改 >>>>>>> 远程分支名
您需要手动编辑这些冲突部分,选择保留哪些修改,或者将两者合并在一起。
-
编辑完毕后,保存文件。
-
运行
git add <file>
将解决冲突后的文件标记为已解决。 -
运行
git commit
提交解决冲突的修改。 -
运行
git stash pop
恢复之前暂存的本地修改。 -
运行
git add <file>
将解决冲突后的文件标记为已解决。 -
运行
git commit -m "Resolve conflict and apply stash"
提交之前暂存的本地修改。 -
运行
git push
推送合并后的代码到远程仓库
你想要返回某个提交状态
$ git log 看你 commit 的日志和提交记录的哈希值
$ git reset xxx(哈希值) 返回相应的提交状态
其他有的没的
$ git diff 查看尚未暂存的更新
$ git rm [file name] 删除一个文件
$ git rm a.a 移除文件(从暂存区和工作区中删除)
$ git remote show 查看远程库的资源
$ git remote show origin 显示远程库 origin 里的资源
$ git config --list 看所有用户 git ls-files 看已经被提交的
遇到的bug
1. await import(‘source-map-support‘).then((r) => r.default.install())
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! dcd@0.0.0 dev: `vite`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the dcd@0.0.0 dev script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\Administrator\AppData\Roaming\npm-cache\_logs\2023-08-07T06_33_32_082Z-debug.log
类似这种的,十之八九是node版本过高或过低,用nvm换一下版本
这里要注意下,由于部门会负责好几个项目,每个项目所需node版本可能会不一样,所以记住这个bug可以在一定程度上避免你心塞,我就是由于这个错误停留了比较久。
项目开始
停送电模块 前端技术栈 vue2 + vuex + less + view-design + ant-design-vue
切页面
分为三个页面,一个记录列表,一个记录表详情,一个配电柜管理,页面方面还比较容易解决,毕竟我着手时已经是二期项目了,直接照着前面的页面依葫芦画瓢就可以了,不过由于之前UI组件库我使用的是vant比较多一点,但UI组件库总体思想差不多,使用view-design也比较顺。
模块功能
- 分页查询停送电记录(按照页码,每页显示数量,状态,开始时间,结束时间来查询), 大致样式如下,都是view-design组件库里的

该图为部分接口文档

- 获取停送电审批状态列表(就是下拉框选项的一系列值,传参也需要用)

- 导出停送电记录列表(按照筛选结果全量导出)

- 根据id查询停送电记详情(就是直接携带入参id跳转页面之后获取)

- 配电柜列表(按照页码,每页显示数量,输入区域/设备编号/名称模糊查询来查询)

- 设备上下线(控制状态)

前端设置代理(企业前端开发跨域处理)
其次,就是在前端设置代理,项目使用的是webpack-dev-server,在这里我就结合整体项目来讲讲吧(没办法,我们leader硬是让我写出个文档说是沉淀)
比如现在后端提供的地址为http://api.example.com
,而我前端的地址为http://127.0.0.1:8080
,那就必定会遇到所谓的跨域问题
之前我的解决办法是,先配置axios的默认请求域名,axios.defaults.baseURL = 'http://api.example.com'
,我使用的是nodejs来搭服务器的,所以配置的access-controll-allow-orgin: http://127.0.0.1:8080
来解决跨域
而到了公司这边,使用的确是webpack-dev-serve来进行代理,相关配置放在webpack.config.js或vite.config.js或vue.config.js中,在这里的配置我就说说比较关键一点的,其他的可以去官网看看webpack-dev-server
module.exports = {
devServer: {
proxy: {
'/hls-pc': {
target: 'http://api.example.com',
changeOrigin: true,
},
},
},
};
再结合一个axios请求来进行理解
axios.get('/hls-pc/getUserInfo')
.then(response => { console.log(response.data); })
.catch(error => { console.error(error); });
当你在前端发送一个axios请求时,你实际的请求地址是http://127.0.0.1:8080/hls-pc/getUserInfo
来进行请求,但这明显是不对的,所以在dev-serer配置中,我们将以 /hls-pc开头的请求代理到target: http://api.example.com
上,即把http://127.0.0.1:8080/hls-pc/getUserInfo
替换(代理)成http://api.example.com/hls-pc/getUserInfo
,也就是把前端主机请求地址中 /hls-pc之前的全部代理成target指向的地址
不过,真正发送请求还得加上changeOrigin: true,它改变的是请求头上Host
字段,
- 如果没有使用
changeOrigin
,代理服务器会将该请求直接转发到后端 API,但是请求头中的Host
字段仍然为127.0.0.1:8080
。后端服务器接收到请求后,会检查请求头中的Host
字段,发现该域名为127.0.0.1:8080
,而不是它预期的后端域名api.example.com
。 - 后端服务器可能会认为这个请求来自一个未授权的域名,出于安全原因,它可能会拒绝这个请求,或者执行一些其他的安全策略。
- 结果就是,前端请求没有正确代理到后端 API,可能会收到跨域错误或拒绝访问等问题。
通过设置 changeOrigin: true
,代理服务器会修改请求头中的 Host
字段,将其改为后端 API 的域名 api.example.com
,这样后端服务器就会正确识别请求的来源,并且能够正常处理请求。
讲了这么多target和changOrigin的配置,基本也算掌握了webpack-dev-server的用法了,然后就是不怎么用的pathRewrite,用法和它的意思一样,重写路径,
module.exports = {
devServer: {
proxy: {
'/hls-pc/getUserInfo': {
target: 'http://api.example.com',
changeOrigin: true,
pathRewrite: {
'^/hls-pc': '',
},
},
}
}
也就是当你如果不希望传递/hls-pc
的时候,则需要重写路径:最后实际请求路径为'http://api.example.com/getUserInfo'
,也就是按照配置'^/hls-pc': ''
,匹配到 /hls-pc的为空
整体代码为
module.exports = {
devServer: {
proxy: {
'/hls-pc/getUserInfo': {
target: 'http://api.example.com',
changeOrigin: true,
pathRewrite: {
'^/hls-pc': '',
},
},
'/hls-pc': {
target: 'http://api.example.com',
changeOrigin: true,
},
}
}
注意,配置的执行顺序是从上到下依次执行的,所以先请求的放到最上面,并且每个配置的target可以指向不同的后端接口地址,相比于access-controll-allow更加的灵活,更适合用于前端开发环境
增加角色,增加视频定制化界面 前端技术栈 vue3 + ant-design-vue + 公司低代码平台
切页面
也就两个简单的表单页面,页面实现比较简单,但也是有两点有点小困难
嵌套的表格

.table-container {
border: 1px solid #cccccc;
width: 100%;
max-width: 700px; /* 设置表格宽度或根据需要调整 */
margin: 16px auto;
.table-row {
display: flex;
border-top: 1px solid #cccccc;
}
.header-row {
background-color: #f2f2f2;
border-top: none;
}
.table-cell {
border-right: 1px solid #cccccc;
padding: 2px;
}
.table-cell1 {
flex: 30%;
}
.table-cell2 {
display: flex;
flex: 70%;
border-right: none;
.table-cell2_inline {
margin: 0 10px;
}
}
}
定制化的表单
这里就是简单的用了下col和row来对其进行分列排布
<a-form-item>
<a-select/>
</a-form-item>
<a-row style="padding: 0 7%">
<a-col class="gutter-row" :span="16">
<a-form-item>
<a-select/>
</a-form-item>
</a-col>
<a-col class="gutter-row" :span="6">
<a-button type="primary">按钮</a-button>
</a-col>
</a-row>
功能模块
新增,编辑,主要是对数据的处理
联调测试
接口对接联调
真正劳心费神的还是接口对接联调,
先说说第一个项目吧,首先后端给了接口文档,这一点是比较好的,在接口文档上能进行调试,但是,遇到了以下问题:
- 后端接口没写完就发布到文档上,导致前端调用时不清楚,返回了错误的信息
- 编写接口文档时参数说明不明确,导致前端不知道发送请求时携带什么参数
- 接口文档中某个接口返回的数据意义不明,即不知道该接口有什么用
- 不是同一个部门的,交流起来比较困难
第二个项目,没有接口文档,因为是基于公司自己开发的低代码平台,后端就随意了许多,比较好的是都是同一个部门的,相处起来比较舒适,不过我得自己熟悉平台,了解里面后端写的逻辑视图之后再使用公司封装的函数进行调用接口
前后端接口联调大致基于上述的问题,不断的和后端反复沟通交流,直到后端真正的把接口写完为止
测试人员进行测试
前端主要是npm run build一下,把代码打包,然后提交给后端
然后后端在服务器上进行部署,并大体测试一遍,把业务逻辑大致走通,然后就是测试人员进行测试,弹出框太小了、文字得换行、放置的位置应该在末尾而不该居中,这些没办法,没给设计稿只给了原型稿,就会出现类似这种问题,测试差不多就上线了
其他代码
总体来说就是我觉得有点意义并且应该使用得次数较多,其实可以把下面的代码看作是面试中的场景题
格式化筛选时间的值,按照年-月-日 时:分:秒来传
// 格式化筛选时间的值,按照年-月-日 时:分:秒来传
timeFormat(s) {
if (!s) {
return undefined;
}
let d = new Date(s);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
const seconds = String(d.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
页面加载时把相应权限复选框勾选,并之后还能进行修改传给后端
这里最关键的部分就是v-model表示勾选值,:checked里的computed函数表示初始状态,@change表示勾选发送的事件。
注意,项目中嵌套了3层,分为了父子孙三级勾选框,分别用page.selected,item.selected,last_item.selected来代替勾选值,比较坑爹的一点就是后端没给selected的值,所以得自己弄 比如按照后端数据初始加载,一开始Facebook和GitHub就会自动勾选

当你另外又勾选了twitter和snapchat后,并不会影响其他勾选值

<div v-for="(page, index) in pages" :key="index">
<input
type="checkbox"
style="margin-left: 20%"
v-model="item.selected"
:checked="initialCheckedItems(item)"
@change="changeSelect(item)"
/>
<div v-for="(item, index2) in page.permission" :key="index2">
<input
type="checkbox"
style="margin-left: 20%"
v-model="item.selected"
:checked="initialCheckedItems(item)"
@change="changeSelect(item)"
/>
<div v-for="(last_item, index) in item.permission" :key="index">
<input
type="checkbox"
style="margin-left: 30%"
v-model="last_item.selected"
:checked="initialCheckedItems(last_item)"
@change="changeSelect(last_item)"
/>
</div>
</div>
</div>
// 初始化该id角色列表权限,并在页面展示出来
const initialCheckedItems = computed(() => {
permissionList = [];
const roleIds = rolePermission.value.map((item) => {
permissionList.push({ id: item.id });
return item.id;
});
return (item) => {
let res = roleIds.includes(item._id);
return res
};
});
// 勾选按钮时的事件
const changeSelect = (last_item) => {
if (last_item.selected) {
// 如果复选框被勾选,则将last_item添加到permissionList数组中
permissionList.push({id: last_item.id });
} else {
// 如果复选框被取消勾选,则从permissionList数组中移除last_item
const index = permissionList.findIndex((item) => item.id === last_item.id);
if (index !== -1) {
permissionList.splice(index, 1);
}
}
// console.log(permissionList);
};
根据状态返回相应的颜色
先在data中写好样式,然后使用三元表达式进行状态的判断
data() {
return {
// 背景色为蓝色,默认色
statusItem1: {
"text-align": "center",
color: " #118ee9",
"font-size": "10px",
width: "60px",
"background-color": " #c9e8fe",
height: "auto",
},
// 背景色为红色,若状态为已拒绝
statusItem2: {
"text-align": "center",
color: " #b90000",
"font-size": "10px",
width: "60px",
"background-color": " #f6c9c9",
height: "auto",
},
// 背景色为绿色,若状态为已上锁
statusItem3: {
"text-align": "center",
color: " #17b900",
"font-size": "10px",
width: "60px",
"background-color": " #d5f6c9",
height: "auto",
},
}
}
computed: {
// 根据状态返回相应的颜色
statusStyle() {
return this.infoData.applyStatus === 0
? this.statusItem2
: this.infoData.applyStatus === 3
? this.statusItem3
: this.statusItem1;
},
}
后端传输文件流,前端进行处理
首先文件流是在res.data中的,然后后端在响应头设置了'content-disposition:' filename=%E8%91%AB%E8%8A%A6%E7%B4%A0%E9%80%89%E7%85%A4%E5%8E%82%E5%AE%89%E5%85%A8%E9%A3%8E%E9%99%A9%E7%AE%A1%E6%8E%A7%E6%B8%85%E5%8D%95.xls
,
对content-disposition进行处理,拿到ASCII码并进行转码和拼接,最后文件名会自动显示到弹窗框中,xxx.xls
export const download = (options) => axios.request({...options, responseType: 'arraybuffer'}).then(res => {
// 发起网络请求,使用 Axios 库,设置 responseType 为 'arraybuffer',以获取文件的二进制数据
const blobData = new Blob([res.data]); // 创建 Blob 对象,用来存储二进制数据
const fileName = res.headers['content-disposition'].split('=')[1]; // 从响应头中获取文件名
let tempArr = fileName.split('.');
let resFileName = decodeURI(tempArr[0]);
tempArr.length > 1 && (resFileName += '.' + tempArr[1]); // 对文件名进行解码,并拼接文件扩展名
const fileUrl = window.URL.createObjectURL(blobData); // 创建一个临时的文件 URL
const link = document.createElement('a'); // 创建一个 <a> 元素
link.href = fileUrl; // 设置链接的 URL 为临时的文件 URL
link.setAttribute('download', resFileName); // 设置下载属性和文件名
link.click(); // 触发点击事件,开始下载
});
该图为文件流

转载自:https://juejin.cn/post/7264556202138009656