API 程序的可扩展性
可扩展性是指系统、程序或技术架构在功能、规模及性能需求增加时,能够容易地增加其性能、容量和功能的能力。
示例
假设我们有一个国际化网站,我们希望根据用户选择的语言动态显示不同的内容,比如在网站首页展示一个欢迎语,不考虑扩展性,可能的实现方式如下:
<!--Vue实现示例-->
<template>
<div>
<h1>
{{ language === "en" ? "Hello" : "你好" }}
</h1>
<div>
{{ language === "en" ? "Welcome to our website!" : "欢迎访问我们网站!" }}
</div>
</div>
</template>
随着公司业务的发展,领导现在要求需要支持法语,如果我们采用上述方式实现这个功能,势必会要进行大量的修改。
这里可以采用策略模式进行修改,代码如下:
const translations = {
zh: {
greeting: "你好",
message: "欢迎访问我们网站!",
},
en: {
greeting: "Hello",
message: "Welcome to our website!",
},
fr: {
greeting: "Bonjour",
message: "Bienvenue sur notre site web!",
},
// other language translations...
};
export default translations;
<!--Vue实现示例-->
<template>
<div>
<h1>
{{ translations[language].greeting }}
</h1>
<div>
{{ translations[language].message }}
</div>
</div>
</template>
程序的 API
API 全称:Application Programming Interface(应用程序编程接口),API 就是一组让别人可以“使用你功能”的“说明书”或“中介”。
示例
<!--Vue选项式 API-->
<script>
export default {
data() {
return {
title: "商品列表",
keyword: "",
list: [
{ id: 1, name: "苹果" },
{ id: 2, name: "香蕉" },
{ id: 3, name: "橙子" },
],
};
},
mounted() {
console.log("组件已挂载,开始加载数据...");
// 模拟请求数据
setTimeout(() => {
this.list.push({ id: 4, name: "梨子" });
}, 1000);
},
computed: {
filteredList() {
if (!this.keyword) return this.list;
return this.list.filter((item) => item.name.includes(this.keyword));
},
},
watch: {
keyword(newVal, oldVal) {
console.log(`关键词从 "${oldVal}" 改成了 "${newVal}"`);
// 可以添加节流搜索等逻辑
},
},
methods: {
search() {
console.log("用户点击了搜索按钮,当前关键词是:", this.keyword);
// 这里可以发起接口请求
},
},
};
</script>
API 的可扩展性设计
开闭原则
- 对扩展开放:当需要添加新功能时,应该尽可能地开放类、模块、函数等的扩展点,通过扩展点给使用者增加新功能的机会。
- 对修改关闭:当需要修改现有功能时,应该尽可能地关闭类、模块、函数等的修改点,尽量通过扩展的方式来变更功能,而不是修改原有功能。
上面的国际化示例就是符合开闭原则的设计。
按需导入
概述:
按需导入方式可支持 tree shaking,有利于生产环境下的性能提升。
import _ from "lodash-es"; // 引入全部
import { cloneDeep } from "lodash-es"; // 按需引入
实现:
// cloneDeep.js
function cloneDeep(value) {}
export default cloneDeep;
// lodash.default.js
import cloneDeep from "./cloneDeep.js";
const lodash = {
cloneDeep,
};
export default lodash;
// lodash.js
export { default as cloneDeep } from "./cloneDeep.js";
Real-world measurements show that import { debounce } … still ships 15–20 kB min-zipped. For true minimal size you must import by path:
import cloneDeep from 'lodash-es/cloneDeep.js' // 1.1 kB gz
配置选项
概述:
灵活的参数传递和默认值设置。
import { gsap } from "gsap";
gsap.to(".box", {
x: 500,
duration: 1, // default: 0.5
delay: 1, // default: 0
stagger: 0.2, // stagger: 0
});
import { gsap } from "gsap";
gsap.to(".box", {
x: 500,
duration: 1,
delay: 1,
stagger: {
each: 0.2,
from: "center", // default: start
},
});
实现:
function to(selector, options) {
const defaults = {
duration: 0.5,
delay: 0,
stagger: 0, // 可以是 number 或对象
};
const config = { ...defaults, ...options };
const elements = document.querySelectorAll(selector);
let totalDelay = config.delay;
const staggerConfig =
typeof config.stagger === "object"
? {
each: config.stagger.each || 0,
from: config.stagger.from || "start",
}
: {
each: config.stagger,
from: "start",
};
const count = elements.length;
let order = Array.from({ length: count }, (_, i) => i);
// 支持 from: 'center'
if (staggerConfig.from === "center") {
const center = (count - 1) / 2;
order = order.sort((a, b) => Math.abs(a - center) - Math.abs(b - center));
}
order.forEach((index, i) => {
const el = elements[index];
const elementDelay = totalDelay + staggerConfig.each * i;
setTimeout(() => {
el.style.transform = `translateX(500px)`;
el.style.transition = `transform ${config.duration}s`;
}, elementDelay * 1000);
});
}
回调映射
概述:
方便的控制每一个元素的自定义行为和返回值。
text.replace(new RegExp(sensitiveWords, "gi"), "*");
// 回调映射
text.replace(new RegExp(sensitiveWords, "gi"), (match) =>
"*".repeat(match.length)
);
import { gsap } from "gsap";
gsap.to(".box", {
x: 500,
});
// 回调映射
gsap.to(".box", {
x: (i, item) => (i + 1) * 200,
});
实现:
function to(selector, options) {
const defaults = {
duration: 0.5,
delay: 0,
stagger: 0,
};
const config = { ...defaults, ...options };
const elements = document.querySelectorAll(selector);
const count = elements.length;
const staggerValue = typeof config.stagger === "number" ? config.stagger : 0;
elements.forEach((el, i) => {
const delay = (config.delay || 0) + staggerValue * i;
// 支持 x 为静态值或函数
let xValue = 0;
if (typeof config.x === "function") {
xValue = config.x(i, el);
} else if (typeof config.x === "number") {
xValue = config.x;
}
setTimeout(() => {
el.style.transition = `transform ${config.duration}s`;
el.style.transform = `translateX(${xValue}px)`;
}, delay * 1000);
});
}
注册机制
概述:
允许在运行时动态地添加或修改程序的行为。
// Vue Router
const router = createRouter({
history: createWebHistory(),
routes: [{ path: "/:articleName", component: Article, name: "article" }],
});
router.addRoute({ path: "/about", component: About });
router.addRoute({ path: "/helper", component: Helper });
router.removeRoute("article");
实现:
function createRouter({ history, routes: initialRoutes }) {
const routes = [...initialRoutes]; // 用数组维护路由表
return {
getRoutes() {
return routes;
},
addRoute(route) {
routes.push(route);
},
removeRoute(name) {
const index = routes.findIndex((route) => route.name === name);
if (index !== -1) {
routes.splice(index, 1);
} else {
console.warn(`未找到名称为 "${name}" 的路由`);
}
},
};
}
拦截器 / 钩子函数
概述:
拦截器和钩子函数的作用类似,用于在特定的时间点或条件下执行自定义逻辑。
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
// 例如,添加认证令牌到请求头
if (store.getters.token) {
config.headers['Authorization'] = Bearer ${store.getters.token}
}
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
if (error.response && error.response.status === 401) {
// 跳转到登录页面或执行其他操作
}
return Promise.reject(error);
});
import { gsap } from "gsap";
gsap.to(".box", {
x: 500,
duration: 2,
onStart(param1, param2) {
console.log("onStart", param1, param2);
},
onStartParams: ["hello", "world"],
onComplete(param1, param2) {
console.log("onComplete", param1, param2);
},
onCompleteParams: ["hello2", "world2"],
onUpdate() {
console.log("onUpdate");
},
});
实现:
function to(selector, options) {
const elements = document.querySelectorAll(selector);
const duration = options.duration || 0.5;
const totalFrames = Math.round(duration * 60);
elements.forEach((el, i) => {
const startTime = performance.now();
const startX = 0;
const endX =
typeof options.x === "function" ? options.x(i, el) : options.x || 0;
// onStart
if (typeof options.onStart === "function") {
options.onStart(...(options.onStartParams || []));
}
let frame = 0;
function animate(now) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / (duration * 1000), 1);
const currentX = startX + (endX - startX) * progress;
el.style.transform = `translateX(${currentX}px)`;
// onUpdate
if (typeof options.onUpdate === "function") {
options.onUpdate();
}
frame++;
if (progress < 1) {
requestAnimationFrame(animate);
} else {
// onComplete
if (typeof options.onComplete === "function") {
options.onComplete(...(options.onCompleteParams || []));
}
}
}
requestAnimationFrame(animate);
});
}
中间件
概述:
中间件一般用来扩展前端框架的能力,功能可以在多个中间件之间进行传递与控制。
// koa中间件
const Koa = require("koa");
const app = new Koa();
app.use(cors());
app.use(serve(__dirname + "/public/"));
app.use((ctx, next) => {
console.log(1);
next();
});
app.use((ctx, next) => {
console.log(2);
});
app.use((ctx, next) => {
console.log(3);
});
app.listen(3000);
实现:
const http = require("http");
class SimpleKoa {
constructor() {
this.middlewares = [];
}
use(fn) {
this.middlewares.push(fn);
}
compose(ctx) {
const dispatch = (i) => {
if (i >= this.middlewares.length) return Promise.resolve();
const middleware = this.middlewares[i];
return Promise.resolve(middleware(ctx, () => dispatch(i + 1)));
};
return dispatch(0);
}
listen(port, callback) {
const server = http.createServer((req, res) => {
const ctx = { req, res };
this.compose(ctx)
.then(() => {
if (!res.headersSent) {
res.end("Done");
}
})
.catch((err) => {
res.statusCode = 500;
res.end("Internal Server Error: " + err.message);
});
});
server.listen(port, callback);
}
}
插件
概述:
插件是一种遵循一定规范的应用程序接口编写出来的程序,它可以扩展或增强主应用程序的功能,而不需要修改主应用程序的源代码。插件通常设计为可以在运行时被动态加载和执行,这使得主应用程序具有很高的灵活性和可扩展性。
import Vue from "vue";
import VueRouter from "vue-router";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
import vFocusNext from "v-focus-next";
Vue.use(VueRouter); //扩展路由
Vue.use(ElementUI); //扩展组件
Vue.use(vFocusNext); //扩展指令
Vue.use(MyPlugin, options);
const MyPlugin = {
install(Vue, options) {
// 添加全局方法或者属性
Vue.myGlobalMethod = function () {
// 逻辑...
};
},
};
实现:
const installedPlugins = [];
function use(plugin, options) {
if (installedPlugins.includes(plugin)) {
console.warn(`Plugin ${plugin.name} has already been installed.`);
return;
}
// 如果插件是个对象,且有install方法,调用它的install方法
if (typeof plugin === "object" && plugin.install) {
plugin.install(Vue, options);
}
// 如果插件是个函数,直接作为installer调用
if (typeof plugin === "function") {
plugin(Vue, options);
}
// 将插件记录为已安装
installedPlugins.push(plugin);
}
柯里化函数
概述:
柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c)
转换为可调用的 f(a)(b)(c)
。柯里化不会调用函数,它只是对函数进行转换。
import { gsap } from "gsap";
document.addEventListener("mousemove", function (e) {
const snapX = gsap.utils.snap(50, e.pageX);
const snapY = gsap.utils.snap(50, e.pageY);
console.log(snapX, snapY);
});
// 柯里化
document.addEventListener("mousemove", function (e) {
const snap50 = gsap.utils.snap(50);
const snapX = snap50(e.pageX);
const snapY = snap50(e.pageY);
console.log(snapX, snapY);
});
实现:
// Check the length of params, if the current length equals to total lenght, then invoke the function, otherwise, return a new function waiting for new params
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function (...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
function sum(a, b, c) {
return a + b + c;
}
let curriedSum = curry(sum);
alert(curriedSum(1, 2, 3)); // 6,仍然可以被正常调用
alert(curriedSum(1)(2, 3)); // 6,对第一个参数的柯里化
alert(curriedSum(1)(2)(3)); // 6,全柯里化
组合函数
概述:
将多个小的函数组合成一个大的函数,使数据依次通过这些函数进行处理。
// without pipe()
var value1 = func1(input);
var value2 = func2(value1);
var output = func3(value2);
// cleaner with pipe()
var transfrom = pipe(func1, func2, func3);
var output = transform(input);
import { gsap } from "gsap";
document.addEventListener("mousemove", function (e) {
const snapX = gsap.utils.snap(50, e.pageX);
const clampX = gsap.utils.clamp(100, 500, snapX);
console.log(clampX);
});
// 函数组合
document.addEventListener("mousemove", function (e) {
const snapAndClampX = gsap.utils.pipe(
gsap.utils.snap(50),
gsap.utils.clamp(100, 500)
);
console.log(snapAndClampX(e.pageX));
});
实现:
function pipe(...fns) {
return function (input) {
return fns.reduce((acc, fn) => {
if (typeof fn !== "function") {
throw new Error("All arguments to pipe must be functions");
}
return fn(acc);
}, input);
};
}
高阶函数
概述:
接受一个或多个函数作为参数,或者返回一个函数作为结果的函数。保留原函数的行为能力并且添加新功能。
import { debounce } from "lodash-es";
let count = 0;
btn.onclick = function () {
div.innerHTML = ++count;
};
// 高阶函数
btn.onclick = debounce(function () {
div.innerHTML = ++count;
}, 200);
实现:
function debounce(fn, delay = 200) {
let timer = null;
return function (...args) {
const context = this;
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
};
}
链式调用
概述:
指的是多个方法连续调用,每次调用返回对象本身或另一个支持继续调用的方法的对象,从而实现简洁、流畅的 API 使用体验。
import { gsap } from "gsap";
const tl = gsap.timeline();
tl.to(".red", {
x: 100,
duration: 1,
});
tl.from(".green", {
x: 200,
duration: 2,
});
tl.set(".blue", {
x: 300,
duration: 3,
});
// 链式调用
const tl = gsap.timeline();
tl.to(".red", {
x: 100,
duration: 1,
})
.from(".green", {
x: 200,
duration: 2,
})
.set(".blue", {
x: 300,
duration: 3,
});
实现:
function createTimeline() {
const queue = [];
const timeline = {
to(selector, config) {
queue.push(() => {
console.log(`to ${selector}`, config);
});
return this;
},
from(selector, config) {
queue.push(() => {
console.log(`from ${selector}`, config);
});
return this;
},
set(selector, config) {
queue.push(() => {
console.log(`set ${selector}`, config);
});
return this;
},
};
return timeline;
}
范围隔离
概述:
可以创建具有独立作用范围的实例,实现“范围隔离”,避免全局污染。
axios.defaults.baseURL = "https://jsonplaceholder.typicode.com/";
axios.defaults.timeous = 1000;
axios.get("/users").then().catch();
axios.get("/todos").then().catch();
// 范围隔离
const instance = axios.create({
baseURL: "https://jsonplaceholder.typicode.com/",
timeout: 1000,
});
instance.get("/users").then().catch();
instance.get("/todos").then().catch();
实现:
const axios = (function () {
const defaults = {
baseURL: "",
timeout: 0,
};
function createAxios(config) {
return {
defaults: config,
get(url) {
const requestConfig = {
baseURL: config.baseURL,
timeout: config.timeout,
url,
};
return Promise.resolve(requestConfig);
},
create(customConfig = {}) {
const mergedConfig = {
...config,
...customConfig,
};
return createAxios(mergedConfig);
},
};
}
return createAxios(defaults);
})();