一、微信小程序
1. 微信小程序文件结构
一个典型的微信小程序项目包含以下核心文件:
- WXML:用于描述页面结构的文件,类似于 HTML。
- WXSS:样式文件,用于描述 WXML 中组件的样式。
- JS:逻辑处理文件,负责处理页面数据、交互以及网络请求。
- JSON:配置文件,用于进行小程序的页面注册、设置页面标题或配置底部 tabBar。
此外,项目根目录下还存在三个特殊的应用级文件:
- app.json:必须存在,是小程序的全局配置文件。它负责注册页面、配置网络超时时间、设置窗口背景色、导航栏样式以及默认标题等。
- app.js:必须存在,用于监听并处理小程序的生命周期函数、声明全局变量。
- app.wxss:可选文件,用于配置所有页面的公共 CSS 样式。
2. 事件传值
在小程序中,可以通过给 WXML 元素添加 data-* 属性来传递自定义数据。在事件处理函数中,可以通过 e.currentTarget.dataset 对象或页面的 onload 函数参数来获取这些值。需要注意的是,data- 后面的属性名不能包含大写字母,并且不能直接存放对象。
3. WXSS 与 CSS 区别
WXSS 在大部分特性上遵循 CSS 标准,但也存在一些区别:
- 图片引入:WXSS 中的背景图片需使用网络地址(外链)。
- 选择器限制:WXSS 中没有
body 元素选择器。
- 样式导入:WXSS 提供了
@import 语句来导入外部样式表。
4. 关联微信公众号确定用户唯一性
当小程序需要与关联的微信公众号共享用户身份时,可以调用 wx.getUserInfo 接口,并将参数 withCredentials 设为 true。成功后会返回一个包含加密用户信息的 encryptedData 字段,其中包含可用于唯一标识用户的 union_id。开发者需要将这个加密数据传递给后端服务器,由后端使用微信提供的会话密钥进行对称解密,从而获得 union_id。
5. 微信小程序与 Vue 区别
尽管微信小程序的开发模式与 Vue 有些相似,但在细节上存在显著差异:
| 对比维度 |
微信小程序 |
Vue |
| 生命周期 |
相对简单,有 onLoad, onShow, onReady 等 |
拥有完整的生命周期钩子函数,如 created, mounted, updated 等 |
| 数据绑定 |
使用 {{}} 语法 |
同样使用 {{}} 语法,但在模板中更常用 : 或 v-bind |
| 显示/隐藏 |
使用 wx:if 和 hidden 属性 |
使用 v-if、v-else 和 v-show 指令 |
| 事件绑定 |
使用 bindtap 或 catchtap(阻止冒泡) |
使用 v-on:event 或 @event 语法糖 |
| 双向绑定 |
需要手动通过事件获取表单元素的值 |
使用 v-model 指令实现数据的自动双向绑定 |
二、Webpack
1. 打包体积优化思路
为了减少最终打包产物的体积,可以采取以下策略:
- 提取第三方库:使用
DllPlugin 或 externals 配置将稳定的第三方库(如 react, lodash)单独分离或通过 CDN 引入。
- 代码压缩:使用
TerserWebpackPlugin(或 UglifyJsPlugin)对 JS 代码进行混淆和压缩。
- 启用 Gzip:在服务器端配置 Gzip 压缩,对传输的资源进行压缩。
- 按需加载:使用
import() 动态导入语法或 require.ensure(Webpack 旧版本)实现路由或组件的按需加载。
- 优化 Source Map:在生产环境选择合适的
devtool 配置(如 source-map),在开发环境选择更快的选项(如 eval-cheap-module-source-map)。
- CSS 分离:使用
MiniCssExtractPlugin 将 CSS 从 JS 中提取出来,单独打包成文件。
- 移除冗余插件:检查并移除生产环境不必要的插件。
2. 打包效率优化
提升 Webpack 构建速度可以从这些方面入手:
- 增量构建与热更新:在开发环境充分利用 Webpack 的缓存和模块热替换(HMR)功能。
- 简化开发构建:在开发环境中跳过提取 CSS、计算文件哈希等耗时操作。
- 合理配置 Loader:通过
test、include 或 exclude 精确指定 Loader 的作用范围,避免不必要的文件处理。
- 启用 Loader 缓存:为
babel-loader 等转换开销大的 Loader 开启缓存 (cacheDirectory: true)。
- 使用新特性与插件:
- 并行处理:使用
thread-loader 将耗时的 Loader 放在独立线程中运行。
- 缓存:使用
cache-loader 或 Webpack 5 自带的持久化缓存。
- 预编译:使用
DllPlugin 将不常变动的第三方库提前打包。
3. Loader 编写
Loader 本质上是一个 Node.js 模块,它导出一个函数。当 Webpack 需要转换某种类型的资源时,就会调用这个函数。在函数内部,可以通过 this 上下文访问 Webpack 提供的 Loader API。
// reverse-txt-loader.js
module.exports = function(src) {
// 这是一个简单的示例:将文本内容反转
var result = src.split('').reverse().join('');
// Loader 需要返回一个 JS 模块代码字符串
return `module.exports = '${result}'`;
}
// 在 webpack.config.js 中使用
module.exports = {
module: {
rules: [
{
test: /\.txt$/,
use: ['./path/reverse-txt-loader']
}
]
}
};
4. Webpack Plugin 与优化
构建过程优化:
- 减少编译范围:使用
ContextReplacementPlugin(用于限定某些库的语言包)、IgnorePlugin(忽略某些模块)。
- 并行压缩:配置
TerserWebpackPlugin 开启多进程并行压缩。
- 预编译链接库:使用
DllPlugin 和 DllReferencePlugin 组合,将第三方库提前编译好,提升后续构建速度。
输出产物优化:
- Tree Shaking:在生产模式下默认启用,移除未使用的代码。
- Scope Hoisting:使用
ModuleConcatenationPlugin(生产模式默认启用)将模块提升到一个作用域内,减少函数声明和内存开销。
- 拆包与缓存:使用
SplitChunksPlugin 智能拆分公共代码和第三方库,并配合 [contenthash] 实现长期缓存。
三、编程题
1. 通用事件侦听器函数
// event(事件)工具集
markyun.Event = {
// 绑定事件 (兼容IE)
addEvent: function(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent('on' + type, function() {
handler.call(element);
});
} else {
element['on' + type] = handler;
}
},
// 移除事件
removeEvent: function(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else if (element.detachEvent) {
element.detachEvent('on' + type, handler);
} else {
element['on' + type] = null;
}
},
// 阻止事件冒泡
stopPropagation: function(ev) {
if (ev.stopPropagation) {
ev.stopPropagation();
} else {
ev.cancelBubble = true;
}
},
// 取消事件默认行为
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
},
// 获取事件目标元素
getTarget: function(event) {
return event.target || event.srcElement;
}
};
2. 判断对象是否为数组
function isArray(arg) {
if (typeof arg === ‘object’) {
return Object.prototype.toString.call(arg) === ‘[object Array]’;
}
return false;
}
3. 冒泡排序
var arr = [3, 1, 4, 6, 5, 7, 2];
function bubbleSort(arr) {
for (var i = 0; i < arr.length - 1; i++) {
for (var j = 0; j < arr.length - i - 1; j++) {
if (arr[j + 1] < arr[j]) {
var temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
console.log(bubbleSort(arr));
4. 快速排序
var arr = [3, 1, 4, 6, 5, 7, 2];
function quickSort(arr) {
if (arr.length == 0) {
return [];
}
var cIndex = Math.floor(arr.length / 2);
var c = arr.splice(cIndex, 1);
var l = [];
var r = [];
for (var i = 0; i < arr.length; i++) {
if (arr[i] < c) {
l.push(arr[i]);
} else {
r.push(arr[i]);
}
}
return quickSort(l).concat(c, quickSort(r));
}
console.log(quickSort(arr));
5. 计算字符串字节长度
function GetBytes(str) {
var len = str.length;
var bytes = len;
for (var i = 0; i < len; i++) {
if (str.charCodeAt(i) > 255) bytes++;
}
return bytes;
}
alert(GetBytes(“你好,as”));
6. bind 函数实现
// 简化版 bind,仅实现上下文绑定
Function.prototype.bind = function(ctx) {
var fn = this;
return function() {
fn.apply(ctx, arguments);
};
};
7. 深拷贝函数
// 方法一:为 Object 原型添加方法 (不推荐,会污染全局原型)
Object.prototype.clone = function() {
var o = this.constructor === Array ? [] : {};
for (var e in this) {
o[e] = typeof this[e] === “object” ? this[e].clone() : this[e];
}
return o;
}
// 方法二:独立的深拷贝函数
function clone(Obj) {
var buf;
if (Obj instanceof Array) {
buf = [];
var i = Obj.length;
while (i—) {
buf[i] = clone(Obj[i]);
}
return buf;
} else if (Obj instanceof Object) {
buf = {};
for (var k in Obj) {
buf[k] = clone(Obj[k]);
}
return buf;
} else {
return Obj;
}
}
8. 点击获取 index(考察闭包)
<ul id=“test”>
<li>这是第一条</li>
<li>这是第二条</li>
<li>这是第三条</li>
</ul>
// 方法一:利用 DOM 对象属性存储索引
var lis = document.getElementById(‘test’).getElementsByTagName(‘li’);
for (var i = 0; i < 3; i++) {
lis[i].index = i; // 将索引存储在 DOM 元素的自定义属性上
lis[i].onclick = function() {
alert(this.index); // 点击时通过 this 访问
};
}
// 方法二:使用闭包保存索引值
var lis = document.getElementById(‘test’).getElementsByTagName(‘li’);
for (var i = 0; i < 3; i++) {
lis[i].onclick = (function(a) {
return function() {
alert(a); // 内部的匿名函数可以访问外部函数的参数 a
}
})(i);
}
9. 代理 console.log 方法
// 单参数版本
function log(msg) {
console.log(msg);
}
// 多参数版本(使用 apply 传递参数列表)
function log() {
console.log.apply(console, arguments);
}
10. 输出今天日期
var d = new Date();
var year = d.getFullYear();
var month = d.getMonth() + 1;
month = month < 10 ? ‘0’ + month : month;
var day = d.getDate();
day = day < 10 ? ‘0’ + day : day;
alert(year + ‘-’ + month + ‘-’ + day);
11. 随机选取并排序
var iArray = [];
function getRandom(istart, iend) {
var iChoice = iend - istart + 1;
return Math.floor(Math.random() * iChoice + istart);
}
for (var i = 0; i < 10; i++) {
iArray.push(getRandom(10, 100));
}
iArray.sort(); // 默认按字符串排序,如需按数值排序应使用 iArray.sort((a,b) => a-b)
12. URL 参数提取
function serilizeUrl(url) {
var result = {};
url = url.split(“?”)[1];
var map = url.split(“&”);
for (var i = 0, len = map.length; i < len; i++) {
result[map[i].split(“=”)[0]] = map[i].split(“=”)[1];
}
return result;
}
13. 清除字符串前后空格
if (!String.prototype.trim) {
// 兼容旧环境,为 String 原型添加 trim 方法
String.prototype.trim = function() {
return this.replace(/^\s+/, “”).replace(/\s+$/, “”);
};
}
var str = “ \t\n test string “.trim();
alert(str == “test string”); // true
14. 间隔输出数字
for (var i = 0; i < 10; i++) {
(function(j) {
setTimeout(function() {
console.log(j + 1);
}, j * 1000);
})(i);
}
15. 判断回文字符串
function run(input) {
if (typeof input !== ‘string’) return false;
return input.split(‘’).reverse().join(‘’) === input;
}
16. 数组扁平化
function flatten(arr) {
return arr.reduce(function(prev, item) {
return prev.concat(Array.isArray(item) ? flatten(item) : item);
}, []);
}
四、其他
1. 负载均衡
负载均衡是指将网络流量或工作任务分配到多台服务器上,以避免单台服务器过载,从而提升整体服务的可用性、响应速度和吞吐量。
常见实现方式:
- HTTP 重定向负载均衡:由调度服务器根据策略,通过 HTTP 302 状态码将客户端请求重定向到目标服务器。缺点是增加了客户端的请求延迟,且调度过程对客户端透明性差。
- DNS 负载均衡:在 DNS 服务器中为一个域名配置多个 IP 地址(A 记录),DNS 服务器根据负载策略返回其中一个 IP。优点是实现简单,但监控性和灵活性较弱。
- 反向代理负载均衡:客户端将请求发往统一的反向代理服务器(如 Nginx),由代理服务器根据负载均衡算法(轮询、加权、IP Hash等)将请求转发给内部的实际应用服务器。对反向代理服务器的性能和稳定性要求较高。
2. CDN
CDN(内容分发网络)通过在全球各地部署边缘节点服务器,将源站内容缓存到离用户更近的节点上。当用户请求资源时,系统会将其导向到距离最近、响应最快的节点,从而避开互联网主干网的拥堵,实现内容的快速、稳定传输。
3. 内存泄漏
定义:在程序中,已动态分配的堆内存由于某些原因未能被释放或无法释放,导致可用内存不断减少,可能引发性能下降或程序崩溃。
JavaScript 中常见的内存泄漏场景:
- 意外的全局变量:未使用
var、let、const 声明的变量会变成全局对象的属性。
- 被遗忘的定时器或回调函数:
setInterval 或事件监听器在组件销毁后未被清除。
- 脱离 DOM 的引用:在 JavaScript 中缓存了某个 DOM 元素的引用,即使该元素已从页面移除,仍无法被垃圾回收。
- 闭包的不当使用:闭包可以维持函数内局部变量,若不慎引用了不再需要的大对象,会导致其无法释放。
避免策略:
- 严格管理变量作用域,避免不必要的全局变量。
- 确保在组件销毁或页面卸载时,清理定时器、事件监听器以及异步回调。
- 谨慎使用闭包,确保其不会长期持有对大对象的引用。
- 对于复杂的单页应用(SPA),可以使用开发者工具的内存分析功能定期检查。
4. Babel 原理
Babel 是一个 JavaScript 编译器,其主要工作流程如下:
- 解析 (Parsing):使用 babylon 解析器将 ES6/ES7 源代码转换成抽象语法树 (AST)。
- 转换 (Transforming):通过 babel-traverse 遍历 AST,并利用配置好的插件 (Plugins) 对树节点进行增删改,将新的 ES6/ES7 语法转换为 ES5 等目标语法。
- 生成 (Generation):最后,由 babel-generator 将转换后的 AST 重新生成为 ES5 代码字符串。
5. JS 自定义事件
在 JavaScript 中创建和触发自定义事件需要三个核心步骤:
- 创建事件:使用
document.createEvent(‘Event’) 方法创建一个新的事件对象。
- 初始化事件:调用事件对象的
initEvent(type, bubbles, cancelable) 方法,定义事件类型、是否冒泡等属性。
- 触发事件:在目标 DOM 元素上调用
dispatchEvent(event) 方法,派发该自定义事件。
6. 前后端路由差别
- 后端路由:每次路由跳转(即使是页面内的链接点击)都会向服务器发起一次新的 HTTP 请求,服务器根据 URL 返回一个完整的 HTML 页面。这种方式在早期多页应用中常见。
- 前端路由:在单页应用(SPA)中,路由的切换由 JavaScript 控制。当 URL 变化时,JS 会拦截这个变化,根据路由映射关系动态地操作 DOM(例如显示/隐藏组件,或请求新的数据与模板进行组合),而不会重新加载整个页面。这提供了更流畅的用户体验。