Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save zzjtnb/a1a358fcbf9ba0928b7723e2ef00e26c to your computer and use it in GitHub Desktop.

Select an option

Save zzjtnb/a1a358fcbf9ba0928b7723e2ef00e26c to your computer and use it in GitHub Desktop.
课程主题是 **项目**,会讲到公司项目研发的流程。
# 课程介绍
正处于春招的黄金时期,为了帮助大家找到更好的工作,更快的找到工作。组织本次的专项课程。
今日的课程主题是 **项目**,会讲到公司项目研发的流程。
因为项目一般**不是**面试时候的核心考察点,所以今天还会讲到一些项目中**经典问题及解法**。
因为**时间有限**, 老师这里准备的都是面试中**最常见的问题**,
以**讲义**内容为主, 如果讲解完毕后仍有时间, 我们再继续扩展其他
# 学习目标
* [x] 理解 promise 主要解决的问题及常规用法
* [x] 能够写出一个可以被链式调用的函数
* [x] 能粗略的实现 promise的基本结构
* [x] 可以自己封装一般的 api 请求模块
* [x] 了解公司一般的角色(岗位)及职责
* [x] 知道一个公司常规项目的流程
* [x] 知道公司常规的缺陷管理的流程
* [x] 知道常规的代码管理方式
* [x] 知道常规系统的登录部分是如何设计的(简单登录/单点登录)
* [x] 知道系统的权限部分如何设计的
* [x] 可以将数组结构的数据转成树结构的数据
* [x] 可以将树结构的数据转成数组的结构
* [x] 知道如何限制接口的访问频率,
* [x] 可以实现简单的 debounce
# 问题列表
| 知识点 |
| ----------------------------------- |
| promise的源码和实际项目使用 |
| 前端后端具体的合作的流程 |
| 从研发到上线的具体详解 |
| 项目开发中遇到的问题 |
| 对axios再次封装(网络请求模块封装) |
| 混合APP原生APP打包上线 |
| 限制接口调用频率 |
| 权限管理结合项目前端实现 |
| 打包到项目部署 |
| 实现登录 |
| 管理项目 |
| 处理树形结构 |
# 面试题详解
## 理解promise
**【问题分析】**
怎么理解 promise ? 写出指定代码的输出内容?
**【答题思路】**
关于 promise 通常会有三种题型。第一种就是问概念;第二种就是 给一段代码让说出他的输出;第三种 就是让你自己实现一个 promise ,来考察你对 promise 的理解和编程能力。(第三种一般是针对中高级的程序员)
本节我们先回顾一下 promise的概念,然后一起来做两个题。
1. promise 概念
**promise 定义**A promise is an object that may produce a single value some time in the future: either a resolved value, or a reason that it’s not resolved (e.g., a network error occurred)
https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-promise-27fc71e77261
https://promisesaplus.com/implementations
2. promise 题目1
```js
数字打印的顺序
const first = () => (new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve(6);
}, 0)
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
}));
first().then((arg) => {
console.log(arg);
});
console.log(4);
//1.同步的代码(最高)
//3
//7
//4
//2. 微任务的异步代码(次高,then)
//1
//2
//3. 宏任务的异步代码(最低,setTimeout)
//5
//6 不执行
```
3. promise 题目2(理解 then 的第二回调函数和 catch)
```js
// 画出以下异步函数执行的可能的路线。
// https://developers.google.com/web/fundamentals/primers/promises
// 蓝线表示执行的 promise 路径,红路表示拒绝的 promise 路径。
// https://developers.google.com/web/fundamentals/primers/imgs/promise-flow.svg
asyncThing1()
.then(function() {
return asyncThing2();
})
.then(function() {
return asyncThing3();
})
.catch(function(err) {
return asyncRecovery1();
})
.then(
function() {
return asyncThing4();
},
function(err) {
return asyncRecovery2();
}
)
.catch(function(err) {
console.log("Don't worry about it");
})
.then(function() {
console.log("All done!");
});
```
![image-20200215153639230](https://tva1.sinaimg.cn/large/0082zybpgy1gbx4j86yaaj30oi0mdwn1.jpg)
## promise 实践
**【问题分析】**
如何把大象🐘放到冰箱里?
**【答题思路】**
开门/装大象/关门 都是异步的操作。
通过本题加强对 promise 的理解,理解 promise 解决的核心问题是什么。
```js
/////////////////方法1 callback//////////////////
//1. 准备函数 openDoor putIn closeDoor done
//2. 执行函数
console.time()
const openDoor=(cb)=>{
setTimeout(cb,1000)
}
const putIn=(cb)=>{
setTimeout(cb,3*1000)
}
const closeDoor=(cb)=>{
setTimeout(cb,1000)
}
const done=()=>{
console.timeEnd();
console.log('done');
}
openDoor(()=> putIn(()=> closeDoor(done) ));
//////
//const openDoor = cb => {
// setTimeout(cb, 1000);
// };
// const promise = () => {
// return new Promise((res, rej) => {
// setTimeout(() => {
// res();
// }, 1000);
// });
// };
/////////////////vs/////////////////////
/////////////////方法1 promise//////////////////
//1. 准备promise函数 openDoor putIn closeDoor done
//2. 执行函数
console.time()
const openDoor=()=>new Promise(res=>{
setTimeout(res,1000)
})
const putIn=()=>new Promise(res=>{
setTimeout(res,1000*3)
})
const closeDoor=()=>new Promise(res=>{
setTimeout(res,1000)
})
const done=()=>new Promise(res=>{
console.timeEnd();
console.log('done2');
res();
})
openDoor()
.then(putIn)
.then(closeDoor)
.then(done)
```
## 如何实现链式调用
**【问题分析】**
类似 `b.then().then().then()` 的链式调用是如何实现的?
**【答题思路】**
因为要不断的调用,所以一定是返回自己,或者返回一个 和自己类似的结构。
后续实现 Promise 的时候,会用得上!
面试就心里强大一点就行;出给我的题 面试官如果不知提前知道答案也 可能答不出来
```js
class Test1{
then(){
console.log(6666);
return this;
}
}
var a= new Test1();
a.then().then().then()
class Test2{
then(){
console.log(77777);
return new Test2();
}
}
var b= new Test2();
b.then().then().then()
```
## promise 单元测试
1.准备测试框架
参考[jest](https://jestjs.io/docs/en/getting-started) 官网
```shell
# 1.初始化项目
npm init
# 2.安装 jest
yarn add --dev jest
#npm install --save-dev jest
# 3. 支持 es2015+ 和 ts
yarn add --dev babel-jest @babel/core @babel/preset-env @babel/preset-typescript @types/jest
# 4. 添加 文件 babel.config.js
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
};
# 5.配置命令
"test": "jest",
"test:watch": "jest --watchAll"
```
2.为了检查 Promise 实现的正确性,我们提前准备好单元测试
```js
import MyPromise from "./index";
// 1
test("1.promise 参数函数会立即执行", function() {
var string;
new MyPromise(function() {
string = "foo";
});
expect(string).toBe("foo");
},500);
it("2. promise 在 then 的回调函数中可以拿到 resolve 的数据。", function(done) {
var testString = "foo";
var promise = new MyPromise(function(resolve) {
setTimeout(function() {
resolve(testString);
}, 20);
});
promise.then(function(string) {
expect(string).toBe(testString);
done();
});
},500);
// 3
it("promise 可以有多个 then,并且会依次执行", function(done) {
var testString = "foo";
var promise = new MyPromise(function(resolve) {
setTimeout(function() {
resolve(testString);
}, 20);
});
promise.then(function(string) {
expect(string).toBe(testString);
});
promise.then(function(string) {
expect(string).toBe(testString);
done();
});
},500);
it("4.promise 可以嵌套多个 then,then的回调中可以返回 promise ", function(done) {
var testString = "foo";
var promise = new MyPromise(function(resolve) {
setTimeout(function() {
resolve();
}, 20);
});
promise
.then(function() {
return new MyPromise(function(resolve) {
setTimeout(function() {
resolve(testString);
}, 20);
});
})
.then(function(string) {
expect(string).toBe(testString);
done();
});
},500);
// 5
it("5.promise 可以嵌套多个 then,then的回调中可以返回 一个普通值", function(done) {
var testString = "foo";
var promise = new MyPromise(function(resolve) {
setTimeout(function() {
resolve();
}, 20);
});
promise
.then(function() {
return testString;
})
.then(function(string) {
expect(string).toBe(testString);
done();
});
},500);
// 6
it("6.resolved 状态的promise ,如果调用 then 方法会立即执行", function(done) {
var testString = "foo";
var promise = new MyPromise(function(resolve) {
setTimeout(function() {
resolve(testString);
}, 20);
});
setTimeout(function() {
promise.then(function(value) {
expect(value).toBe(testString);
done();
});
}, 200);
},500);
it("7. 二次调用 resolve 不会产生影响。", function(done) {
var testString = "foo";
var testString2 = "bar";
var promise = new MyPromise(function(resolve) {
setTimeout(function() {
resolve(testString);
resolve(testString2);
}, 20);
});
promise.then(function(value) {
expect(value).toBe(testString);
});
setTimeout(function() {
promise.then(function(value) {
expect(value).toBe(testString);
done();
});
}, 50);
},500);
```
## promise 源码
**【问题分析】**
如何实现一个 Promise ?
**【答题思路】**
promise 是一个对象,一般是通过 new Promise ()来实例化的;所以这里我要实现 Promise 类!
promise 的 then 是可以链式调用的,所以可能会用到上面提到的,链式调用的实现。
根据逐个单元测试的要求来实现 Promise
主要实现 Promise 的构造方法和 then 方法; 后面会以链接的方式给出完整的实现。
**【解题代码】**
```ts
const State = {
pending: "pending",
resolved: "rejected",
rejected: "rejected"
};
const noop = () => {};
class MyPromise {
constructor(exclutor) {
exclutor(this._resolve.bind(this), this._reject);
}
_state = State.pending;
_value;
_resolve(val) {
if (this._state === State.pending) {
this._value = val;
this._state = State.resolved;
this._runResolveArray();
}
}
_reject() {}
_runResolveArray() {
//执行 then 传入进来的 onRes
this._resArray.forEach(item => {
// const item
const result = item.handle(this._value);
const nextPromise = item.promise;
if (result instanceof MyPromise) {
result.then(val => item.promise._resolve(val));
} else {
item.promise._resolve(result);
}
});
}
_resArray = [];
then(onRes, onRej = noop) {
// if (this._state === State.pending) {
const newPromise = new MyPromise(() => {});
const item = { promise: newPromise, handle: onRes };
this._resArray.push(item);
// }
if (this._state === State.resolved) {
this._runResolveArray();
}
return newPromise;
}
}
export default MyPromise;
```
【问题延伸】
几种Promise 的实现
https://github.com/ericyang89/my-promise
https://github.com/vividbytes/implementing-promises
https://github.com/iam91/zpromise/blob/master/src/zpromise.js
## 项目api 请求模块封装
**【题目分析】**
你项目中的网络/api 请求模块是如何封装的?
**【答题思路】**
1. 接口请求一般是异步的,可以返回 promise 更加清晰。
2. 网络请求url 的公共部分可以单独配置到 网络请求内部。
3. 针对所有的接口可以进行统一的处理。这也是面向切面编程的一个实践。
4. 可以借助第三方库 axios 快速的封装 网络请求模块。
**【解题步骤|代码】**
```ts
import axios from "axios";
import constant from "../constant";
import reactNavigationHelper from "./reactNavigationHelper";
import commonToast from "./commonToast";
//配置请求url 的公共部分及超时时间
const commonHttp = axios.create({
baseURL: constant.baseUri,
timeout: 10 * 1000
});
commonHttp.interceptors.response.use(
function(response) {
return response;
},
function(error) {
//针对所有接口统一处理登录过期的问题
if (error.response.status === 401) {
commonToast.show("登录过期");
reactNavigationHelper.navigate("Login");
}
return Promise.reject(error);
}
);
export default commonHttp;
```
## 公司角色及职责
一般公司与项目流程有关的角色
![image-20200219142325265](https://tva1.sinaimg.cn/large/0082zybpgy1gc1ow4ov3nj30jy0hhn0u.jpg)
![image-20200219142343405](https://tva1.sinaimg.cn/large/0082zybpgy1gc1owg6le3j30jo0heq6o.jpg)
##项目流程
**【题目分析】**
你们的开发流程/项目流程大概是什么样的?
**【答题思路】**
![image-20200219142531400](https://tva1.sinaimg.cn/large/0082zybpgy1gc1oybv0y5j30u00wbn9k.jpg)
![image-20200219142820684](https://tva1.sinaimg.cn/large/0082zybpgy1gc1p19c4iej30vc0rugvg.jpg?1)
![image-20200219142841929](https://tva1.sinaimg.cn/large/0082zybpgy1gc1p1mqu2dj30vd0b3juh.jpg)
![image-20200219150052792](https://tva1.sinaimg.cn/large/0082zybpgy1gc1pz3xe5hj30pm0tuq7t.jpg)
简要说明项目流程:
1. 产品出需求文档;开需求会;
2. 估时,部分项目写技术方案;接口阅读
3. 开始开发;测试给冒烟测试/测试用例评审
4. 前后端联调
5. 冒烟测试
6. 提测
7. 跟测/上环境
8. 上线/线上跟踪/项目总结
## 项目缺陷管理
**【题目分析】**
如何做bug/缺陷管理?
**【答题思路】**
1. 缺陷管理角色
![image-20200219144941335](https://tva1.sinaimg.cn/large/0082zybpgy1gc1pngtxqej30to0neadm.jpg)
![image-20200219145001216](https://tva1.sinaimg.cn/large/0082zybpgy1gc1pnsvg2ej30tl06dt9o.jpg)
2. bug 处理流程
![image-20200219144601063](https://tva1.sinaimg.cn/large/0082zybpgy1gc1pjnhgvgj31930u0qjw.jpg)
![image-20200219144725016](https://tva1.sinaimg.cn/large/0082zybpgy1gc1pl3odp7j30y00s4wng.jpg)
## 代码管理
**【题目分析】**
你们的代码大概怎么管理的?
怎么管理 git 分支的?
**【答题思路】**
![image-20200219150915511](https://tva1.sinaimg.cn/large/0082zybpgy1gc1q7u7w0sj317r0smh0y.jpg)
## 项目打包
**【题目分析】**
你们项目为什么需要打包/编译/构建?
**【答题思路】**
为什么要项目打包?
1. es2015+ 转 es5
2. Js/css/图片 文件合并
3. ts 转 js
4. 模板编译 jsx 转 js
5. less/sass 转 css
6. css
7. 代码压缩
8. 针对不同环境生成不同的代码
## 常规登录流程
**【题目分析】**
你们项目中登录大概是怎么实现?
**【答题思路】**
1. 最简单的登录流程
![image-20200219204350033](https://tva1.sinaimg.cn/large/0082zybpgy1gc1zvzkgwxj30m50b676h.jpg)
2. 端点登录-sso
![image-20200219210517079](https://tva1.sinaimg.cn/large/0082zybpgy1gc20ibpe75j30kv0q3juv.jpg)
3. 注意问题
1. 密码一般不用明文保存
2. 发送 用户名 密码 的网络请求一般不用 get
## 项目中权限管理
**【题目分析】**
你们项目中权限管理是怎么实现?
**【答题思路】**
1. 常规权限模型
RBAC基于角色的权限访问控制(Role-Based Access Control)
![image-20200220112421950](https://tva1.sinaimg.cn/large/0082zybpgy1gc2pc8el7wj30jg0bhjv8.jpg)
![image-20200220112449243](https://tva1.sinaimg.cn/large/0082zybpgy1gc2pcqn4y5j30ji0algqp.jpg?)
2. 常规管理系统中的权限
+ 根据登录用户返回菜单列表— — 可能是一颗树
+ 根据登录用户控制接口权限
3. 前台系统中的权限管理
+ 更专注于入口的控制
+ 越权的提示也不可少
+ 多级权限可能会涉及到计算
**【问题延伸】**
菜单列表通常是一个树的结构,后端一般喜欢给一个列表(数组),该如何去构建?!
## 根据数组构建树
**【题目分析】**
根据数组构建树?
**【答题思路】**
1. 给定原始数据(originData),期待生成一个树形结构的对象(treeData)。
```ts
export type Item = {
id: number;
parentId: number;
name: String;
};
export type Node = Item & { children: Node[] };
export const originData: Item[] = [
{
id: 0,
parentId: null,
name: "生物"
},
{
id: 1,
parentId: 0,
name: "动物"
},
{
id: 2,
parentId: 0,
name: "植物"
},
{
id: 3,
parentId: 0,
name: "微生物"
},
{
id: 4,
parentId: 1,
name: "哺乳动物"
},
{
id: 5,
parentId: 1,
name: "卵生动物"
},
{
id: 6,
parentId: 2,
name: "种子植物"
},
{
id: 7,
parentId: 2,
name: "蕨类植物"
},
{
id: 8,
parentId: 4,
name: "大象"
},
{
id: 9,
parentId: 4,
name: "海豚"
},
{
id: 10,
parentId: 4,
name: "猩猩"
},
{
id: 11,
parentId: 5,
name: "蟒蛇"
},
{
id: 12,
parentId: 5,
name: "麻雀"
}
];
export const treeData: Node = {
children: [
{
children: [
{
children: [
{ children: [], id: 8, name: "大象", parentId: 4 },
{ children: [], id: 9, name: "海豚", parentId: 4 },
{ children: [], id: 10, name: "猩猩", parentId: 4 }
],
id: 4,
name: "哺乳动物",
parentId: 1
},
{
children: [
{ children: [], id: 11, name: "蟒蛇", parentId: 5 },
{ children: [], id: 12, name: "麻雀", parentId: 5 }
],
id: 5,
name: "卵生动物",
parentId: 1
}
],
id: 1,
name: "动物",
parentId: 0
},
{
children: [
{ children: [], id: 6, name: "种子植物", parentId: 2 },
{ children: [], id: 7, name: "蕨类植物", parentId: 2 }
],
id: 2,
name: "植物",
parentId: 0
},
{ children: [], id: 3, name: "微生物", parentId: 0 }
],
id: 0,
name: "生物",
parentId: null
};
```
2. 单元测试
```ts
import arrayToTree from "./arrayToTree";
import { originData, treeData } from "./data";
test("arrayToTee", () => {
expect(arrayToTree(originData)).toEqual(treeData);
});
```
3. 实现步骤
+ 实现函数签名
+ 实现函数体
+ 处理 root
+ 实现 addChildren 方法
+ 执行 addChildren
**【解题步骤】**
```ts
import { Item, Node } from "./data";
// + 实现函数签名
// + 实现函数体
// + 处理 root
// + 实现 addChildren 方法
// + 执行 addChildren
const arrayToTree = (arr) => {
if (!Array.isArray(arr) || arr.length < 1) return null;
const [root] = arr.filter(item => item.parentId === null);
const addChildren = (node, dataList) => {
const children = dataList
.filter(item => item.parentId === node.id)
.map(item => addChildren(item, dataList));
return { ...node, children };
};
return addChildren(root, arr);
};
export default arrayToTree;
```
## 树的遍历
**【题目分析】**
把上一节树的数据转成数组?
**【答题思路】**
1. 单元测试
```ts
import treeToArray from "./treeToArray";
import { originData, treeData } from "./data";
test("arrayToTee", () => {
expect(
treeToArray(treeData).sort((a, b) => (a.name > b.name ? 1 : -1))
).toEqual(originData.sort((a, b) => (a.name > b.name ? 1 : -1)));
});
```
2. 大概步骤
+ 编写函数框架
+ 实现 `const nodeToArray = (node: Node, result: Item[]) => {}`
+ 调用 nodeToArray
**【解题步骤】**
```ts
import { Node, Item } from "./data";
const treeToArray = (node: Node) => {
const nodeToArray = (node: Node, arr: Item[]) => {
const { children, ...item } = node;
arr.push(item);
children.forEach(child => nodeToArray(child, arr));
return arr;
};
return nodeToArray(node, []);
};
export default treeToArray;
```
**【问题延伸】**
+ 二叉树遍历 https://leetcode.com/problems/binary-tree-preorder-traversal/
+ 二叉树层序遍历 https://leetcode-cn.com/problems/binary-tree-level-order-traversal/
+ bfs
## 如何限制接口访问频次
**【题目分析】**
如何限制接口访问频次?
**【答题思路】**
接口请求部分的逻辑我们可以封装成一个函数 fun。假设存在一个处理函数的函数better,接受fun,返回一个 betterFun。当 betterFun 被多次调用,fun 只在不要的时候执行。
`const betterFun=better(fun);`
限制接口访问一般涉及到用户的输入。主要有两个经典场景:`debounce` 强制函数在某段时间内只执行一次,`throttle` 强制函数以固定的速率执行。 [对比demo](https://css-tricks.com/the-difference-between-throttling-and-debouncing/)
1. 使用 debounce(防抖) https://codepen.io/llh911001/pen/EVMLJw?editors=101
2. 使用 throttle (截流) https://codepen.io/llh911001/pen/XmGYKV?editors=101
debounce 单元测试
```ts
import debounce from "./debounce";
jest.useFakeTimers();
test("debounce", () => {
const func = jest.fn();
const debounced = debounce(func, 1000);
debounced();
debounced();
jest.runAllTimers();
expect(func).toHaveBeenCalledTimes(1);
});
test("debounce twice", () => {
const func = jest.fn();
const debounced = debounce(func, 1000);
debounced();
debounced();
setTimeout(debounced, 20);
setTimeout(debounced, 2000);
jest.runAllTimers();
expect(func).toHaveBeenCalledTimes(2);
});
```
**【解题步骤】**
实现 debounce
```ts
const debounce = (fn, delay: number) => {
let inDebounce;
return function(...args) {
const context = this;
clearTimeout(inDebounce);
inDebounce = setTimeout(() => {
fn.apply(context, args);
}, delay);
};
};
export default debounce;
```
**【问题延伸】**
1. 实现 throttle
```ts
/**
*
* @param fn {Function} 实际要执行的函数
* @param delay {Number} 执行间隔,单位是毫秒(ms)
*
* @return {Function} 返回一个“节流”函数
*/
function throttle(fn, threshold) {
// 记录上次执行的时间
var last
// 定时器
var timer
// 默认间隔为 250ms
threshold || (threshold = 250)
// 返回的函数,每过 threshold 毫秒就执行一次 fn 函数
return function () {
// 保存函数调用时的上下文和参数,传递给 fn
var context = this
var args = arguments
var now = +new Date()
// 如果距离上次执行 fn 函数的时间小于 threshold,那么就放弃
// 执行 fn,并重新计时
if (last && now < last + threshold) {
clearTimeout(timer)
// 保证在当前时间区间结束后,再执行一次 fn
timer = setTimeout(function () {
last = now
fn.apply(context, args)
}, threshold)
// 在时间区间的最开始和到达指定间隔的时候执行一次 fn
} else {
last = now
fn.apply(context, args)
}
}
}
```
2. debounce.leading vs debounce.trailing
debounce.leading demo: https://codepen.io/dcorb/pen/GZWqNV
debounce.trailing demo: https://codepen.io/dcorb/pen/KVxGqN
```ts
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
```
参考:
https://css-tricks.com/the-difference-between-throttling-and-debouncing/
http://hackll.com/2015/11/19/debounce-and-throttle/index.html
https://zhuanlan.zhihu.com/p/50367441
http://demo.nimius.net/debounce_throttle/
## 前后端联调注意事项
【题目分析】
你们公司(上一家公司),一般是怎么进行前后端联调的?
【答题思路】
1. 项目是前后端分离的项目;后端负责提供数据接口,前端负责页面。
2. 一般接口完成之后,后端会给一份接口文档,包含接口地址,请求类型,请求的参数,可能的数据返回。
3. 部分后端同学可能不写文档,会进行口头沟通。口头沟通的时候需要记录必要的信息。
4. 联调的时候最好是工作时间(免得晚上,别人下班了),因为可能涉及到沟通。
5. 最好开发之前想好你需要哪些数据,然后和后端提前沟通一下。避免一下你需要的数据,后端没有开发,导致项目工期延误。
## 复习js(可选)
[讲义](./how javascript run.md)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment