1、微信小程序普通分包
- 声明
subpackages后,将按subpackages配置路径进行打包,subpackages配置路径外的目录将被打包到主包中 - 主包也可以有自己的
pages,即最外层的pages字段。 subpackage的根目录不能是另外一个subpackage内的子目录tabBar页面必须在主包内- 假设小程序目录结构如下:
├── app.js ├── app.json ├── app.wxss ├── moduleA │ └── pages │ ├── rabbit │ └── squirrel ├── moduleB │ └── pages │ ├── pear │ └── pineapple ├── pages │ ├── index │ └── logs └── utils - 配置
{ "pages":[ "pages/index", "pages/logs" ], "subpackages": [ { "root": "packageA", "pages": [ "pages/cat", "pages/dog" ] }, { "root": "packageB", "name": "pack2", "pages": [ "pages/apple", "pages/banana" ] } ] }
2、微信小程序独立分包
- 介绍
-
独立分包是小程序中一种特殊类型的分包,可以独立于主包和其他分包运行。从独立分包中页面进入小程序时,不需要下载主包。当用户进入普通分包或主包内页面时,主包才会被下载。 -
开发者可以
按需将某些具有一定功能独立性的页面配置到独立分包中。当小程序从普通的分包页面启动时,需要首先下载主包;而独立分包不依赖主包即可运行,可以很大程度上提升分包页面的启动速度。 -
一个小程序中可以有
多个独立分包。
-
- 使用独立分包时要注意
- 独立分包中
不能依赖主包和其他分包中的内容,包括js 文件、template、wxss、自定义组件、插件等(使用 分包异步化 时 js 文件、自定义组件、插件不受此条限制) 主包中的 app.wxss 对独立分包无效,应避免在独立分包页面中使用 app.wxss 中的样式;- App 只能在主包内定义,独立分包中不能定义 App,会造成无法预期的行为;
- 独立分包中暂时不支持使用插件。
- 独立分包中
- 配置方法
- 配置
{ "pages": [ "pages/index", "pages/logs" ], "subpackages": [ { "root": "moduleA", "pages": [ "pages/rabbit", "pages/squirrel" ] }, { "root": "moduleB", "pages": [ "pages/pear", "pages/pineapple" ], "independent": true // 启动独立分包 } ] }
- 配置
微信小程序将公共组件放到分包中
- 通常情况下,
公共组件需要放在主包中,因为分包不会在应用启动时立即下载。 - 如果将公共组件放在分包中,可能会
因分包尚未下载而导致组件无法使用,出现报错。 - 若确实需要将公共组件放在分包中,可以使用微信小程序的
preloadRule预加载规则。通过预加载规则,可以在进入指定页面前提前下载分包。 - 在 app.json 中配置
preloadRule,指定哪些页面需要预加载哪些分包。这样,当进入预加载规则中的页面时,分包会被提前下载,分包中的公共组件就可以在当前包中正常使用。
{
"subPackages": [
{
"root": "subpackage",
"pages": [
"page1",
"page2"
]
}
],
"preloadRule": {
"pages/index/index": {
// all: 不限网络,wifi: 仅wifi下预下载
"network": "wifi",
// 当进入首页的时候,提前预渲染 subpackage 分包的组件
"packages": ["subpackage"]
}
}
}
微信小程序的登录流程
- 小小程序端调用
login接口获取用户的code。 - 后端接收前端传来的
code,并附上appid、secret、grant_type等参数,请求微信的登录校验接口。 - 微信登录校验接口返回
session_key、unionid、openid等信息。 - 后端根据获取到的
openid和业务系统进行登录处理。
3、异步的理解
- 异步编程是指在
执行过程中,不同任务之间不需要等待上一个任务执行完毕就可以开始执行,而是通过回调函数、Promise、async/await等机制来处理。异步编程可以在处理网络请求、文件读写、定时任务等需要等待时间的操作时发挥重要作用。- 回调函数:最早的异步编程方式是使用回调函数。通过将需要异步执行的任务放在回调函数中,在任务完成时调用该回调函数来处理结果。
setTimeout(function() { console.log("This is a callback function."); }, 1000); - Promise:Promise 是一种更加灵活和强大的异步编程方式,它提供了更好的处理异步操作和错误处理的机制。
let promise = new Promise(function(resolve, reject) { setTimeout(function() { resolve("Promise resolved."); }, 1000); }); promise.then(function(result) { console.log(result); }); - async/await:async/await 是 ES2017 引入的语法糖,使得异步代码的书写更加简洁和易读。通过 async 声明函数为异步函数,在异步操作前使用 await 关键字来等待异步操作的结果。
async function fetchData() { let response = await fetch('https://api.example.com/data'); let data = await response.json(); return data; }
- 回调函数:最早的异步编程方式是使用回调函数。通过将需要异步执行的任务放在回调函数中,在任务完成时调用该回调函数来处理结果。
4、JS排序
- 冒泡排序
function bubbleSort(arr) {
let len = arr.length;
for (let i = 0; i < len; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
let temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
let array = [64, 34, 25, 12, 22, 11, 90];
console.log(bubbleSort(array)); // 输出: [11, 12, 22, 25, 34, 64, 90]
- 快速排序
function quickSort(arr) {
if (arr.length <= 1) {
return arr;
}
let pivot = arr[0];
let left = [];
let right = [];
for (let i = 1; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat(pivot, quickSort(right));
}
let array = [64, 34, 25, 12, 22, 11, 90];
console.log(quickSort(array)); // 输出: [11, 12, 22, 25, 34, 64, 90]
- 插入排序
function insertionSort(arr) {
let len = arr.length;
for (let i = 1; i < len; i++) {
let key = arr[i];
let j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key;
}
return arr;
}
let array = [64, 34, 25, 12, 22, 11, 90];
console.log(insertionSort(array)); // 输出: [11, 12, 22, 25, 34, 64, 90]
- 倒序
function insertionSort(arr) {
let len = arr.length;
for (let i = 1; i < len; i++) {
let key = arr[i];
let j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key;
}
return arr.reverse(); // 倒序输出
}
let array = [64, 34, 25, 12, 22, 11, 90];
console.log(insertionSort(array)); // 输出: [90, 64, 34, 25, 22, 12, 11]
- 一串数字,实现3位一个逗号的
function addCommas(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
let number = 1234567890;
console.log(addCommas(number)); // 输出: "1,234,567,890"
// 字符串递归方法
function formatNumber(num, chart=',', length=3){
let result = ''
let nums = num.toString().split('.')
let int = nums[0]
let decmial = nums[1] ? '.' + nums[1] : ''
let index = 0
for (let n = int.length - 1; n >= 0; n--) {
index ++
result = int[n] + result
if (index % length === 0 && n !== 0) {
result = chart + result
}
}
return result + decmial
}
console.log(formatNumber(2223456789.123)) // 2,223,456,789.123
5、事件循环
- JavaScript事件循环是JavaScript引擎处理异步任务的机制,它确保了JavaScript是单线程执行的同时,能够处理异步任务和事件。事件循环的基本原理如下:
-
执行同步任务:首先,JavaScript引擎会执行当前的同步任务,这些任务会按照它们在代码中的顺序立即执行。
-
处理异步任务:如果有异步任务,比如定时器、事件监听器的回调函数、Promise的异步操作等,它们不会立即执行,而是被放入相应的任务队列中。
-
事件循环:当主线程的同步任务执行完成后,JavaScript引擎会进入事件循环(Event Loop)阶段。在事件循环中,JavaScript引擎会不断地从任务队列中取出任务,并执行它们。这个过程是循环的,直到任务队列为空。
-
任务队列:任务队列分为宏任务队列(macrotask queue)和微任务队列(microtask queue)。通常,每个事件循环中会优先执行微任务队列中的任务,然后再执行宏任务队列中的任务。
-
微任务:微任务通常包括Promise的异步操作、MutationObserver的回调等。微任务会在当前事件循环中的所有同步任务执行完成后立即执行,而且会在下一个事件循环之前完成。
-
宏任务:宏任务包括setTimeout、setInterval、DOM事件等。宏任务会在当前事件循环的所有微任务执行完成后执行,而且它们的执行时间通常会在下一个事件循环开始之前。
-
6、小程序蓝牙api简单的使用
- 初始化模块、扫描周边设备
// 初始化蓝牙模块 openBluetoothAdapter() { wx.openBluetoothAdapter({ success: (res) => { console.log('openBluetoothAdapter success', res) this.startBluetoothDevicesDiscovery() }, fail: (res) => { if (res.errCode === 10001) { wx.onBluetoothAdapterStateChange(function (res) { console.log('onBluetoothAdapterStateChange', res) if (res.available) { this.startBluetoothDevicesDiscovery() } }) } } }) } // 扫描设备 startBluetoothDevicesDiscovery() { if (this._discoveryStarted) { // 只进行一次扫描 return } this._discoveryStarted = true wx.startBluetoothDevicesDiscovery({ allowDuplicatesKey: true, success: (res) => { console.log('startBluetoothDevicesDiscovery success', res) this.onBluetoothDeviceFound() }, }) }, - 发现周边的蓝牙设备
// 周边的蓝牙设备信息列表 onBluetoothDeviceFound() { wx.onBluetoothDeviceFound((res) => { res.devices.forEach(device => { if (!device.name && !device.localName) { return } const foundDevices = this.data.devices const idx = inArray(foundDevices, 'deviceId', device.deviceId) const data = {} if (idx === -1) { data[`devices[${foundDevices.length}]`] = device } else { data[`devices[${idx}]`] = device } console.log(data) }) }) }, - 连接蓝牙设备
// 用户点击连接的蓝牙设备 createBLEConnection(e) { const ds = e.currentTarget.dataset const deviceId = ds.deviceId // 蓝牙设备的唯一标识符 const name = ds.name wx.createBLEConnection({ deviceId, success: (res) => { this.setData({ connected: true, name, deviceId, }) this.getBLEDeviceServices(deviceId) } }) // 停止搜寻附近的蓝牙外围设备 this.stopBluetoothDevicesDiscovery() }, // 获取服务id getBLEDeviceServices(deviceId) { wx.getBLEDeviceServices({ deviceId, success: (res) => { for (let i = 0; i < res.services.length; i++) { if (res.services[i].isPrimary) { // uuid 用于标识蓝牙设备的服务和特征值,以便进行数据交换和通信 this.getBLEDeviceCharacteristics(deviceId, res.services[i].uuid) return } } } }) }, getBLEDeviceCharacteristics(deviceId, serviceId) { // 获取蓝牙设备的特征值 wx.getBLEDeviceCharacteristics({ deviceId, serviceId, success: (res) => { console.log('success', res.characteristics) for (let i = 0; i < res.characteristics.length; i++) { let item = res.characteristics[i] if (item.properties.read) { // 读取蓝牙设备的数据 wx.readBLECharacteristicValue({ deviceId, serviceId, characteristicId: item.uuid, success: (res) => { console.log('读取成功', JSON.stringify(res)); }, fail: (res) => { console.log('读取失败', JSON.stringify(res)); } }) } if (item.properties.write) { let buffer = new ArrayBuffer(1) let dataView = new DataView(buffer) dataView.setUint8(0, Math.random() * 255 | 0) // 向蓝牙设备写入数据 wx.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId: item.uuid, value: buffer, success:(res)=>{ console.log('写入成功', res); }, fail:(res)=>{ console.log('写入失败', res); }, }) } if (item.properties.notify || item.properties.indicate) { // 监听蓝牙设备特征值变化事件 wx.notifyBLECharacteristicValueChange({ deviceId, serviceId, characteristicId: item.uuid, state: true, }) } } }, fail(res) { console.error('getBLEDeviceCharacteristics', res) } }) // 操作之前先监听,保证第一时间获取数据 wx.onBLECharacteristicValueChange((characteristic) => { const idx = inArray( this.data.chs, 'uuid', characteristic.characteristicId ) const data = {} if (idx === -1) { data[`chs[${this.data.chs.length}]`] = { uuid: characteristic.characteristicId, value: ab2hex(characteristic.value) } } else { data[`chs[${idx}]`] = { uuid: characteristic.characteristicId, value: ab2hex(characteristic.value) } } console.log(data) }) },
7、小程序中,蓝牙与硬件设备传递数据的格式
-
原始数据:在某些情况下,可以直接传输原始的字节数据(例如 ArrayBuffer)。这种方式适用于自定义的通信协议,需要在设备和小程序之间进行严格的协议约定。
-
JSON 数据:如果设备和小程序之间支持 JSON 格式的数据交换,可以使用 JSON 格式来传输数据。这种方式简单易用,适用于传输结构化的数据,如传感器数据、控制命令等。
-
自定义协议:有时候会根据具体的需求设计自定义的通信协议,包括数据的格式、命令的定义等。在这种情况下,需要设备端和小程序端都遵循相同的协议规范,以确保数据能够正确地解析和处理。
-
特定格式的数据包:某些设备可能使用特定格式的数据包进行通信,例如 Modbus、CAN 等工业领域常见的通信协议。在这种情况下,需要根据设备的通信协议来解析和构建数据包。
8、请描述一下前端文件上传的基本流程。
-
创建文件上传表单: 在 HTML 页面中创建一个表单,用于用户选择要上传的文件。通常使用
<form>元素,设置enctype属性为"multipart/form-data",以支持文件上传。在表单中添加一个<input type="file">元素,这是用户选择文件的输入框。可能会添加一些其他的表单字段来传递其他数据。 -
用户选择文件: 用户在页面中点击文件上传输入框,然后选择要上传的文件。选择完成后,文件选择框中会显示所选文件的名称。
-
获取文件信息: 通过 JavaScript 可以获取用户选择的文件的相关信息,比如文件名、大小、类型等。这些信息可以在上传前进行验证或显示给用户。
-
提交表单: 当用户点击上传按钮或者表单提交时,浏览器会将表单数据(包括文件数据)发送到服务器。这个过程可以通过表单的 submit 事件或者 JavaScript 中的 AJAX 方法来触发。
-
上传文件: 一旦表单提交,浏览器会将文件数据上传到服务器。在这个过程中,浏览器会将文件数据打包成一个 HTTP 请求,发送给服务器。服务器接收到请求后,会根据业务逻辑进行文件处理,比如保存文件到服务器本地或者进行其他操作。
-
处理上传结果: 服务器处理完文件上传请求后,会向浏览器返回一个 HTTP 响应。浏览器接收到响应后,可以根据情况显示上传结果给用户,比如上传成功、上传失败或者其他信息。
9、如何通过 js 获取用户选择的文件的信息,比如文件名、大小、类型等?
- 获取文件名: 可以通过文件对象的 name 属性获取文件的名称。
- 获取文件大小: 文件对象的 size 属性返回文件的字节大小。
- 获取文件类型: 文件对象的 type 属性返回文件的 MIME 类型。
- 获取最后修改时间: 文件对象的 lastModifiedDate 属性返回文件的最后修改时间。
10、如何在上传文件时实现上传进度的监控?
-
创建 XMLHttpRequest 对象或使用 Fetch API: 首先,你需要使用 XMLHttpRequest 对象或 Fetch API 发送文件上传请求到服务器。Fetch API 更加现代化和简洁,但是 XMLHttpRequest 在一些旧版本的浏览器中仍然有广泛支持。
-
监听上传进度事件: 在发送上传请求之前,你需要添加一个事件监听器来监控上传进度。在 XMLHttpRequest 中,你可以监听 progress 事件。而在 Fetch API 中,你可以使用 fetch 函数的返回值中的 Body 对象来监控上传进度。
-
更新上传进度条或显示上传进度信息: 当上传进度发生变化时,你可以获取上传进度信息,并将其用于更新页面上的上传进度条或者显示上传进度信息。
const fileInput = document.getElementById('fileInput');
const uploadButton = document.getElementById('uploadButton');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
uploadButton.addEventListener('click', function() {
const file = fileInput.files[0];
if (file) {
const xhr = new XMLHttpRequest();
// 监听上传进度事件
xhr.upload.addEventListener('progress', function(event) {
if (event.lengthComputable) {
const progress = (event.loaded / event.total) * 100;
progressBar.value = progress;
progressText.textContent = `上传进度: ${progress.toFixed(2)}%`;
}
});
// 上传文件
xhr.open('POST', 'upload.php', true);
const formData = new FormData();
formData.append('file', file);
xhr.send(formData);
}
});
11、如何使用 Blob 对象处理文件数据?
-
Blob(Binary Large Object)对象是 JavaScript 中用来表示二进制数据的一种数据类型。Blob 对象通常用于处理文件数据、图像数据、音频/视频数据等。你可以使用Blob 对象进行多种操作,包括读取、处理和传输文件数据。 -
创建 Blob 对象: 可以使用 Blob 构造函数创建一个 Blob 对象,它接受一个数据数组和一个参数对象,用于指定数据类型。
const data = ['Hello', 'world']; const blob = new Blob(data, { type: 'text/plain' }); -
读取 Blob 数据: 可以使用 FileReader 对象读取 Blob 对象中的数据。
const fileReader = new FileReader(); fileReader.onload = function(event) { const data = event.target.result; console.log(data); // 打印 Blob 数据 }; fileReader.readAsText(blob); -
将 Blob 数据转换为 URL: 可以使用 URL.createObjectURL() 方法将 Blob 对象转换为一个 URL,用于在页面中显示文件内容,比如图片、音频或视频。
const blobURL = URL.createObjectURL(blob); -
获取 Blob 对象的大小、类型等信息: Blob 对象有一些属性可以获取相关信息。
const size = blob.size; // 获取 Blob 对象的大小,单位为字节 const type = blob.type; // 获取 Blob 对象的 MIME 类型 -
在 Blob 对象中进行切片:可以使用 slice() 方法从 Blob 对象中提取指定范围的数据块,返回一个新的 Blob 对象。
const sliceBlob = blob.slice(start, end, contentType);
12、什么是 Blob URL?如何释放 Blob URL?
-
Blob URL(Object URL)是一种特殊的 URL:,用于将 Blob 或 File 对象表示为可用于在页面中显示的 URL。它提供了一种在浏览器中直接访问 Blob 或 File 对象数据的方式,通常用于显示图片、音频、视频等内容。
-
Blob URL 的格式为 blob:
<origin>/<uuid>,其中<origin>表示URL的源,通常是当前页面的源,<uuid>是一个唯一标识符,用于标识特定的Blob或File对象。 -
释放 Blob URL:在使用完
Blob URL后,为了释放资源并防止内存泄漏,需要手动释放Blob URL。释放Blob URL可以通过调用URL.revokeObjectURL()方法来实现,传递要释放的Blob URL作为参数。const blob = new Blob(['Hello, world!'], { type: 'text/plain' }); const blobURL = URL.createObjectURL(blob); // 使用 blobURL 来显示或处理 Blob 对象的数据 // 释放 Blob URL URL.revokeObjectURL(blobURL);
13、如何实现大文件的分片上传?
-
切片: 将大文件切割成小的数据块(分片),每个数据块的大小通常为固定值或者根据需求动态调整。切片可以在客户端(前端)或者服务器端(后端)完成,常见的做法是在客户端完成切片以减轻服务器负担。
-
上传: 将每个数据块分别上传到服务器。这可以通过多个并行的 HTTP 请求来实现,每个请求负责上传一个数据块。
-
合并: 服务器收到所有数据块后,将它们按照顺序合并成完整的文件。合并可以在服务器端完成。
-
处理并发和错误: 在上传和合并过程中,需要处理并发上传的情况,确保数据块按照正确的顺序合并,并且需要处理上传过程中可能出现的错误,比如网络中断、上传失败等情况。
const fileInput = document.getElementById('fileInput');
const uploadButton = document.getElementById('uploadButton');
const chunkSize = 10 * 1024 * 1024; // 10MB
uploadButton.addEventListener('click', function() {
const file = fileInput.files[0];
if (file) {
const totalChunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
// 递归上传分片
function uploadChunk(start, end) {
const formData = new FormData();
formData.append('file', file.slice(start, end));
fetch('uploadChunk.php', {
method: 'POST',
body: formData
})
.then(response => {
if (response.ok) {
currentChunk++;
if (currentChunk < totalChunks) {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
uploadChunk(start, end);
} else {
console.log('文件上传完成');
}
} else {
console.error('上传失败');
}
})
.catch(error => {
console.error('上传出错', error);
});
}
// 开始上传第一个分片
const start = 0;
const end = Math.min(chunkSize, file.size);
uploadChunk(start, end);
}
});
14、在大文件上传中,如何保证数据的完整性和一致性?
-
使用校验和或哈希值: 在上传文件之前,可以对文件数据计算校验和或哈希值,并将其与文件一起上传到服务器。在服务器端接收到文件后,重新计算文件的校验和或哈希值,然后与客户端提供的值进行比较,以确保文件在传输过程中没有被篡改。
-
分片上传和校验: 将大文件切分成小的数据块(分片)进行上传,每个分片上传完成后都进行校验和或哈希值的计算,并将结果一同上传到服务器。在服务器端接收到所有分片后,可以对每个分片进行校验,然后再进行合并操作。
-
使用上传令牌和验证: 在上传文件时,为每个文件生成一个唯一的上传令牌,并将上传令牌一同上传到服务器。在服务器端接收到文件后,验证上传令牌的有效性,以确保文件上传请求是合法的。这可以防止恶意的文件上传请求,并提高数据的安全性和完整性。
-
使用文件版本号或时间戳: 在文件上传完成后,服务器端可以为上传的文件生成一个版本号或者时间戳,并将其返回给客户端。客户端可以保存这个版本号或时间戳,以后如果需要验证文件的完整性,可以再次发送这个版本号或时间戳到服务器进行验证。
-
传输层加密: 使用安全的传输协议(如HTTPS)进行文件上传,以确保文件在传输过程中的安全性和完整性。通过传输层加密,可以防止中间人攻击和数据篡改。
15、Three.js 的主要功能和特点是什么?
-
简化 WebGL 开发: Three.js 封装了底层的 WebGL API,使得开发者可以更轻松地使用 JavaScript 创建和渲染复杂的3D场景,而不需要直接操作 WebGL 的复杂性。
-
跨平台和跨浏览器兼容性: Three.js 提供了高度的跨平台和跨浏览器兼容性,可以在不同的操作系统和设备上运行,并在主流的现代浏览器中表现良好。
-
丰富的功能和组件: Three.js 提供了丰富的功能和组件,包括几何体、材质、灯光、相机、渲染器等,以及支持加载和显示3D模型、纹理贴图、动画、物理引擎等高级特性。
-
高性能: Three.js 在底层优化了 WebGL 渲染引擎,以提供高性能的3D渲染效果,能够有效地处理大量的几何体和复杂的场景。
16、了解 Three.js 中的场景、相机、渲染器、几何体、材质、灯光等基本概念?
-
场景(Scene):
- 场景是 Three.js 中包含所有3D对象的容器。它是一个三维空间,可以包含模型、灯光、相机等元素。在场景中,可以设置背景色、雾效等属性。
-
相机(Camera):
- 相机定义了观察场景的视角和范围。Three.js 支持不同类型的相机,包括透视相机(PerspectiveCamera)和正交相机(OrthographicCamera)。透视相机类似于人眼的视角,近大远小;正交相机则没有远近之分,对象大小不会随距离变化。
-
渲染器(Renderer):
- 渲染器负责将 Three.js 场景渲染到浏览器的画布上。Three.js 提供了几种渲染器,包括 WebGLRenderer、WebGL2Renderer、CanvasRenderer 等,其中 WebGLRenderer 是最常用的,使用 WebGL 技术进行渲染。
-
几何体(Geometry):
- 几何体定义了3D对象的形状,比如立方体、球体、圆柱体等。Three.js 提供了许多内置的几何体,也支持通过定义顶点和面来创建自定义的几何体。
-
材质(Material):
- 材质定义了3D对象的外观和质地。它决定了对象如何反射光线和显示颜色。Three.js 提供了多种内置的材质,包括基础材质、 Lambert 材质、Phong 材质、Standard 材质等。
-
灯光(Light):
- 灯光用于照亮场景中的对象。Three.js 支持多种类型的灯光,包括环境光(AmbientLight)、点光源(PointLight)、聚光灯(SpotLight)、方向光(DirectionalLight)等。
17、了解如何在 Three.js 中加载和显示3D模型?比如OBJ、GLTF等格式?
- 在 Three.js 中加载和显示3D模型可以通过加载器(Loader)来实现。Three.js 提供了一系列的加载器,用于加载不同格式的3D模型,包括 OBJ、GLTF、FBX 等。下面以 OBJ 格式和 GLTF 格式为例说明如何加载和显示3D模型:
- 加载和显示 OBJ 格式的3D模型:
// 创建场景、相机和渲染器 const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // 创建加载器 const loader = new THREE.OBJLoader(); // 加载OBJ格式的模型 loader.load( 'model.obj', // 模型文件的路径 function (obj) { // 添加到场景中 scene.add(obj); }, function (xhr) { // 加载过程中的回调函数 console.log((xhr.loaded / xhr.total * 100) + '% 已加载'); }, function (error) { // 加载失败的回调函数 console.error('加载失败', error); } ); // 渲染场景 function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate(); - 加载和显示 GLTF 格式的3D模型:
// 创建场景、相机和渲染器 const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // 创建加载器 const loader = new THREE.GLTFLoader(); // 加载GLTF格式的模型 loader.load( 'model.gltf', // 模型文件的路径 function (gltf) { // 添加到场景中 scene.add(gltf.scene); }, function (xhr) { // 加载过程中的回调函数 console.log((xhr.loaded / xhr.total * 100) + '% 已加载'); }, function (error) { // 加载失败的回调函数 console.error('加载失败', error); } ); // 渲染场景 function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate();
18、了解如何在 Three.js 中创建复杂的材质和着色器效果?
-
自定义着色器(Shader): Three.js 允许你使用自定义的着色器来创建复杂的材质效果。你可以编写顶点着色器和片元着色器,来定义对象的外观和表面特性。
-
创建着色器材质(ShaderMaterial): 使用 Three.js 的 ShaderMaterial 类,你可以将自定义的着色器与 Three.js 的渲染管线集成起来,从而实现更复杂的材质效果。
// 创建场景、相机和渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 定义自定义的顶点着色器和片元着色器
const vertexShader = `
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
}
`;
// 创建着色器材质
const material = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader
});
// 创建一个立方体几何体
const geometry = new THREE.BoxGeometry();
// 创建一个立方体网格对象,并应用自定义的着色器材质
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 设置相机位置
camera.position.z = 5;
// 渲染场景
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
19、了解如何在 Three.js 中实现阴影、物理引擎、后期处理等高级特性?
阴影:
- Three.js 支持在场景中添加阴影,包括平行光(DirectionalLight)、点光源(PointLight)、聚光灯(SpotLight)等。要启用阴影,需要将 shadow 属性设置为 true,并调整阴影的相关参数,如阴影分辨率、阴影投射和接收等。
物理引擎:
- Three.js 中并没有内置的物理引擎,但可以通过第三方库来实现物理效果,比如 Ammo.js、Cannon.js 等。这些库可以用来模拟物体之间的碰撞、重力、运动等物理效果,从而增加场景的真实感。
后期处理:
- Three.js 中可以通过使用 PostProcessing(后期处理)库来实现一些后期效果,比如景深效果、泛光、色调映射等。PostProcessing 库提供了一系列的后期处理效果,可以用来增强场景的视觉效果。
20、如何在 Three.js 中创建不同类型的灯光,比如环境光、点光源、聚光灯等?
- 在 Three.js 中,可以使用不同类型的灯光来照亮场景中的对象。常见的灯光类型包括
环境光(AmbientLight)、点光源(PointLight)、聚光灯(SpotLight)、方向光(DirectionalLight)等。下面是使用不同类型的灯光的示例代码: - 环境光(AmbientLight):
- 环境光是一种均匀分布的光源,不会产生阴影,用于模拟场景中的整体光照效果。
// 颜色、强度 const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); scene.add(ambientLight);
- 环境光是一种均匀分布的光源,不会产生阴影,用于模拟场景中的整体光照效果。
- 点光源(PointLight):
- 点光源是一种类似于灯泡的光源,可以向所有方向发射光线,会产生阴影。
// 颜色、强度、距离 const pointLight = new THREE.PointLight(0xffffff, 1, 100); // 设置光源位置 pointLight.position.set(0, 10, 0); scene.add(pointLight);
- 点光源是一种类似于灯泡的光源,可以向所有方向发射光线,会产生阴影。
- 聚光灯(SpotLight):
- 聚光灯是一种具有方向性的光源,可以通过设置位置和方向来控制光线的照射范围,会产生锥形的光照效果。
const spotLight = new THREE.SpotLight(0xffffff, 1, 100, Math.PI / 4, 1, 1); // 颜色、强度、距离、角度、衰减因子、阴影 spotLight.position.set(0, 10, 0); // 设置光源位置 spotLight.target.position.set(0, 0, 0); // 设置光源照射的目标对象 scene.add(spotLight); scene.add(spotLight.target); // 必须将目标对象添加到场景中
- 聚光灯是一种具有方向性的光源,可以通过设置位置和方向来控制光线的照射范围,会产生锥形的光照效果。
- 方向光(DirectionalLight):
- 方向光是一种平行光源,类似于太阳光,光线是平行的,不会因为距离而衰减,可以产生阴影。
// 颜色、强度 const directionalLight = new THREE.DirectionalLight(0xffffff, 1); // 设置光源位置 directionalLight.position.set(0, 10, 0); scene.add(directionalLight);
- 方向光是一种平行光源,类似于太阳光,光线是平行的,不会因为距离而衰减,可以产生阴影。
21、什么是 Promise?它解决了什么问题?
Promise是一种用于处理异步操作的对象。它代表了一个异步操作的最终完成(或失败)及其结果值。Promise提供了更直观和更强大的方式来处理回调地狱问题和链式异步操作。
22、Promise 的三种状态是什么?
Pending(待定):初始状态,既没有被兑现,也没有被拒绝。Fulfilled(已兑现):操作成功完成。Rejected(已拒绝):操作失败。
23、Promise 的 .then() 和 .catch() 方法有什么作用?
.then()用于处理成功的操作,它接收两个回调函数,第一个是成功时调用,第二个是失败时调用(可选)。.catch()用于处理失败的操作,它是 .then(null, errorHandler) 的语法糖。
24、如何使用 Promise.all() 和 Promise.race()?
Promise.all()用于等待所有Promise都完成,如果有一个失败,则整个返回的Promise失败。Promise.race()用于等待第一个完成的Promise,无论成功还是失败。
25、Promise 中的 finally 方法有什么作用?
finally方法用于在Promise结束时,无论结果是成功还是失败,执行一些清理操作。
26、Promise 和 async/await 的区别是什么?
Promise是用于处理异步操作的对象,而async/await 是基于 Promise 的语法糖。- 使用
async/await可以使异步代码看起来像同步代码,增加可读性。
27、手写一个promise,如何实现?该注意什么?简略说一下
- 状态管理:一个 Promise 有三种状态:pending、fulfilled 和 rejected。
- then 方法:实现 then 方法,用于注册成功和失败的回调。
- 异步执行:Promise 的回调应该异步执行,确保符合规范。
- 状态变更:状态只能从 pending 变为 fulfilled 或 rejected,且一旦改变就不能再变。
- 处理链式调用:then 方法返回一个新的 Promise,从而实现链式调用。
class MyPromise {
constructor(executor) {
// 初始状态为pending
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
// resolve 函数,用于将 Promise 状态转变为 fulfilled
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(callback => callback(this.value));
}
};
// reject 函数,用于将 Promise 状态转变为 rejected
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(callback => callback(this.reason));
}
};
// 立即执行 executor 函数,并捕获可能抛出的异常
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
// then 方法,用于注册 fulfilled 和 rejected 状态的回调函数
then(onFulfilled, onRejected) {
// 确保 onFulfilled 和 onRejected 是函数,否则赋予默认函数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
// 返回一个新的 Promise 对象,用于支持链式调用
const promise2 = new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.state === 'rejected') {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.state === 'pending') {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
}
});
return promise2;
}
}
// resolvePromise 函数,用于处理 then 方法返回的结果 x
function resolvePromise(promise2, x, resolve, reject) {
// 如果 promise 和 x 是同一个对象,抛出类型错误,避免循环引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
// 判断是否已经调用过 resolve/reject,避免重复调用
let called = false;
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
try {
const then = x.then;
if (typeof then === 'function') {
// 如果 x 是一个 Promise,继续递归解析
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, err => {
if (called) return;
called = true;
reject(err);
});
} else {
resolve(x);
}
} catch (error) {
if (called) return;
called = true;
reject(error);
}
} else {
resolve(x);
}
}
28、常见的 CSS 选择器
- 通配符选择器 (*): 选择所有元素。
* { color: blue; } - 类型选择器(元素选择器): 选择所有指定类型的元素。
p { color: red; } - 类选择器: 选择所有包含指定类的元素。
.class-name { color: green; } - ID 选择器: 选择具有指定 ID 的元素。
#id-name { color: purple; } - 属性选择器: 选择具有指定属性的元素。
[type="text"] { color: orange; } - 伪类选择器: 选择处于特定状态的元素。
a:hover { color: yellow; } - 伪元素选择器: 选择元素的特定部分。
p::first-line { color: pink; } - 后代选择器: 选择指定元素的所有后代元素。
div p { color: brown; } - 子选择器: 选择指定元素的所有直接子元素。
div > p { color: blue; } - 相邻兄弟选择器: 选择指定元素之后的相邻兄弟元素。
h1 + p { color: green; } - 通用兄弟选择器: 选择指定元素之后的所有兄弟元素。
h1 ~ p { color: orange; }
29、CSS 权重问题
- CSS 权重由四个部分组成,可以用四位数表示 (a, b, c, d)。每个部分代表不同类型的选择器:
- 权重排序规则
- 内联样式:最高权重 (1, 0, 0, 0)
- ID 选择器:其次 (0, 1, 0, 0)
- 类选择器、属性选择器、伪类选择器:再次 (0, 0, 1, 0)
- 类型选择器、伪元素选择器:最低 (0, 0, 0, 1)
- 如果遇上
!important:!important的优先级高于任何不包含!important的样式声明,即使这些声明的选择器权重较高- 当多个规则都使用了
!important,就会比较它们的选择器权重,权重高的规则优先级更高
30、uniapp 和 taro 这类框架,为什么能编译成小程序、H5这些呢?
1. 编译原理
-
抽象层(Abstraction Layer):
UniApp和Taro提供了一个抽象层,开发者在这个抽象层上进行开发。这个抽象层包括通用的API、组件和框架,使得开发者可以编写与平台无关的代码。 -
适配层(Adapter Layer):
适配层负责将抽象层上的代码转换成各个平台的代码。适配层中包含了不同平台的适配器,用于处理平台之间的差异性。 -
编译流程:
-
代码编写:开发者编写基于抽象层的代码,这些代码使用了统一的API和组件。 -
代码解析:编译器解析开发者编写的代码,生成中间表示(Intermediate Representation, IR)。中间表示(IR) 是编译器技术中的一个概念。它是一种介于源代码和目标代码之间的抽象形式,方便编译器进行各种优化和代码生成。
-
平台转换:根据目标平台,编译器将中间表示转换成对应平台的代码。例如,将Vue代码转换成微信小程序的WXML和WXSS,或转换成H5的HTML和CSS。 -
生成产物:编译器生成目标平台的代码产物,这些产物可以直接运行在目标平台上。
-
2. 多端适配
UniApp和Taro提供了一套通用的API和组件,开发者可以通过这些API和组件进行开发,而不需要关心具体平台的实现细节。通用API和组件在编译时会映射到对应平台的 API 和组件。例如:- 调用
uni.request在不同平台上对应微信小程序的wx.request、H5 的fetch等。 - 使用
uni.navigateTo实现页面跳转,在不同平台上会映射到相应的导航 API。
- 调用
- 平台差异处理
- 对于一些
特定的平台差异,框架提供了平台特定的代码块和条件编译来处理。开发者可以通过条件编译语法在代码中写入不同平台的特定实现。
// #ifdef MP-WEIXIN // 微信小程序特有的代码 wx.showToast({ title: 'Hello WeChat' }); // #endif // #ifdef H5 // H5 平台特有的代码 alert('Hello H5'); // #endif - 对于一些
3. 编译器和运行时
-
编译器
- 编译器是
UniApp和Taro的核心部分,负责将通用的代码转换成目标平台的代码。编译器包括以下几个部分:解析器:解析源代码,生成抽象语法树(AST)。转换器:将抽象语法树转换成目标平台的中间表示。生成器:将中间表示生成目标平台的代码。
- 编译器是
-
运行时
- 运行时
部分负责在目标平台上执行生成的代码,并提供一些通用的功能,例如状态管理、事件处理等。运行时会根据平台的特性进行优化和调整,以保证应用的性能和兼容性。
- 运行时
31、async、await的设计和实现。
1、Promise:
async/await是构建在promise之上的。promise是一种表示异步操作最终完成或失败的对象。它有三种状态:pending、fulfilled、rejected。async函数总是返回一个promise对象。function asyncTest() { return new Promise((resolve, reject) => { // 异步操作 // 如果成功,调用 resolve(result) // 如果失败,调用 reject(error) }) }
2、async函数:
async函数是用于定义异步操作的函数。在函数体内部,可以使用await关键字等待一个promise解决,而不会阻塞其他代码的执行。async function asyncFunc() { try { let result = await asyncTest() console.log(result) } catch (error) { console.error(error) } }
3、设计和实现
async / await的设计目标是简化异步代码的书写和理解。它使得开发者能够以类似同步的方式编写异步代码,避免了回调地狱和提高了代码可读性。- 实际上,
async/await是promise的语法糖,它背后仍然依赖于promise的机制。async函数会返回一个promise对象,而await表达式会暂停执行async函数,等待promise的解决。当promise解决后,await表达式将返回解决的值,并继续执行async函数。 - 在现代的浏览器和
Node.js环境中,async/await已得到广泛支持,使得异步代码的编写更加方便。这种设计和实现方式旨在提高异步代码的可维护性和可读性,同时减少错误的可能性。
32、动态导入
-
动态导入(Dynamic Import)是JS的一种功能,允许在运行时按需加载模块,而不是在脚本开始执行时静态地导入所有模块。动态导入通过调用import()函数来实现,该函数返回一个Promise,因此可以与async/await语法结合使用,使代码更加灵活和高效。 -
按需加载:
- 动态导入可以按需加载模块,减少初始加载时间,提高应用性能。例如,在单页面应用(SPA)中,可以根据用户操作按需加载页面组件。
if (condition) { import('./moduleA.js').then(module => { module.doSomething(); }); } else { import('./moduleB.js').then(module => { module.doSomethingElse(); }); } -
延迟加载:
- 可以延迟加载某些模块,直到它们真正需要时才加载,优化资源利用。
async function init() { const { initModule } = await import('./initModule.js'); initModule(); // 延迟加载其他模块 setTimeout(async () => { const { otherModule } = await import('./otherModule.js'); otherModule.doSomething(); }, 1000); } init(); -
条件加载:
- 根据运行时条件加载不同的模块。
async function loadComponent(componentName) { const component = await import(`./components/${componentName}.js`); return component.default; } loadComponent('Header').then(Header => { // 使用 Header 组件 }); -
分割代码:
- 在大型应用中,可以使用动态导入来分割代码,减少单个文件的大小,提升加载速度和响应速度。
async function loadPage(pageName) { const page = await import(`./pages/${pageName}.js`); page.render(); } // 根据路由加载不同的页面 loadPage('home'); -
动态导入的优势
-
提高性能:动态导入允许按需加载模块,减少初始加载时间和内存占用。 -
增强灵活性:可以根据运行时的条件和用户行为加载不同的模块,提升用户体验。 -
支持异步操作:动态导入返回 Promise,可以与 async/await 结合使用,简化异步代码的编写。
-
33、import 怎么实现的
-
模块解析:当 JavaScript 引擎遇到 import 语句时,首先会解析模块路径,确定要导入的模块。 -
模块加载:引擎通过模块加载器加载指定的模块。模块加载器会检查模块是否已经加载过,如果是,则直接返回已加载的模块。 -
模块执行:加载器解析模块内容,并执行模块代码。对于尚未执行的模块,会按照模块依赖关系进行递归加载和执行。 -
导出内容:模块执行完成后,它会导出指定的内容。导出的内容将会被其他模块引用和使用。
34、CI/CD流程的具体步骤
-
代码提交: 开发人员将代码推送到版本控制系统(如GitHub)。
-
触发构建: CI工具检测到新的提交后,触发构建流程。
-
依赖安装: 安装项目依赖(如通过npm或yarn)。
-
静态代码分析: 运行ESLint等工具进行代码质量检查。
-
单元测试: 运行单元测试,确保代码功能正确。
-
构建: 将代码打包为生产环境可用的文件(如webpack打包)。
-
部署: 将构建后的文件部署到测试环境或生产环境。 通知:通过电子邮件、Slack等方式通知相关人员构建和部署结果。
35、什么是FOUC(无样式内容闪烁)?你如何来避免FOUC?
-
FOUC 是指在网页加载过程中,用户在短暂的时间内看到未应用样式的内容。这种现象通常发生在 CSS 样式表未完全加载之前,浏览器先渲染了未应用样式的 HTML 内容,然后再应用样式,导致内容闪烁。
-
避免方法:
-
确保所有的 CSS 样式表都放在
<head>标签内,这样浏览器在解析 HTML 时可以优先加载 CSS,防止未应用样式的内容渲染。 -
尽量避免在 CSS 文件中使用
@import语句,因为这会延迟样式的加载。可以通过 标签一次性加载所有 CSS 文件。 -
将关键的 CSS 直接内联到 HTML 文档的
<head>部分。这种方法可以确保关键样式在页面加载时立即应用,从而减少 FOUC。 -
使用
async或defer属性来延迟 JavaScript 的加载和执行,以便让浏览器优先处理 CSS 和 HTML。这有助于减少 FOUC。 -
对于 React、Vue 等前端框架,可以使用服务器端渲染(SSR)技术。在服务器上预先渲染 HTML 和 CSS,从而在客户端加载时减少 FOUC。
-
36、什么是requestAnimationFrame?
-
它是一个浏览器提供的高效 API,用于在浏览器的下一次重绘之前执行动画更新。它提供了一种更流畅和高效的方式来进行动画,使开发者可以精确地同步代码执行与屏幕的刷新率,从而创建平滑的动画效果。
-
高效动画:
- requestAnimationFrame 会在浏览器准备下一次重绘时调用指定的回调函数,从而使动画与浏览器的刷新频率同步。
- 浏览器的刷新频率通常是每秒 60 次(60Hz),因此回调函数大约每 16.7 毫秒执行一次。
-
节能和优化:
- 在隐藏或不可见的标签页中,requestAnimationFrame 会暂停调用,这样可以节省 CPU 和电池资源。这相比于 setInterval 或 setTimeout 更为节能。
-
平滑的动画:
- 通过同步到屏幕刷新,requestAnimationFrame 能够提供更平滑的动画效果,减少由于动画帧和屏幕刷新不匹配而产生的卡顿或跳帧。
37、sass 和 less 的区别。
1. 语法
- Sass 有两种语法:缩进语法(.sass 文件)和 SCSS 语法(.scss 文件)。
- Sass
-
缩进语法(Sass):
- 无需大括号 {} 和分号 ;。
- 使用缩进表示层级关系。
$primary-color: #333 body color: $primary-color -
SCSS 语法:
- 与 CSS 语法几乎相同。
- 使用大括号 {} 和分号 ;。
$primary-color: #333; body { color: $primary-color; }
-
- Less
- Less 的语法更接近 CSS,只需扩展一些特性。
@primary-color: #333; body { color: @primary-color; }
2. 变量
- Sass 使用 $ 符号定义变量。
$primary-color: #333;
- Less 使用 @ 符号定义变量。
@primary-color: #333;
3. 混合(Mixins)
-
Sass
- 支持参数和默认值。
- 可以使用 @include 调用。
@mixin border-radius($radius) { -webkit-border-radius: $radius; -moz-border-radius: $radius; -ms-border-radius: $radius; border-radius: $radius; } .box { @include border-radius(10px); } -
Less
- 支持参数和默认值。
- 直接调用混合。
.border-radius(@radius) { -webkit-border-radius: @radius; -moz-border-radius: @radius; -ms-border-radius: @radius; border-radius: @radius; } .box { .border-radius(10px); }
4. 函数
- Sass
- Sass 提供更强大的内置函数库,如颜色操作、数学运算、字符串操作等。
$base-color: #036; $lighter-color: lighten($base-color, 20%); - Less
- Less 也提供了一些内置函数,但相对较少。
@base-color: #036; @lighter-color: lighten(@base-color, 20%);
5. 插值
- Sass
- Sass 支持字符串插值,可以将变量值嵌入到选择器或属性值中。
$name: "main"; .#{$name} { color: blue; } - Less
- Less 也支持插值,但语法略有不同。
@name: "main"; .@{name} { color: blue; }
6. 控制指令
- Sass
- Sass 提供了更丰富的控制指令,如 @if, @for, @each, @while 等。
@for $i from 1 through 3 { .item-#{$i} { width: 2em * $i; } } - Less
- Less 也支持条件语句和循环,但语法和功能上稍逊于 Sass。
.loop (@i) when (@i > 0) { .item-@{i} { width: (@i * 2em); } .loop(@i - 1); } .loop(3);
7. 扩展性
- Sass:由于其语法和功能的丰富性,Sass 通常被认为在大型项目中更具可扩展性。
- Less:Less 更接近 CSS,对于那些希望逐步增强现有样式的项目来说,可能更容易上手。
8. 编译工具
- Sass:官方提供了多种编译工具,如 Dart Sass 和 LibSass,也有广泛的第三方工具支持。
- Less:Less 也有官方的编译器和多种第三方工具支持,但相对来说选择较少。