前端工程化是一个系统工程,涵盖了从需求分析、开发、构建、部署到发布、监控的全流程。通过规范流程、使用脚手架、进行体验度量和稳定性检测,可以显著提高前端开发的效率和质量,确保产品在生产环境中的稳定运行。
规范流程
研发效能
团队能够持续地为用户产生有效价值的效率,包括有效性(Effectiveness)、效率(Efficiency)和可持续性(Sustainability)三个方面。简单来说,就是能否长期、高效地交付出有价值的产品;
对于常规开发阶段而言,我们经常直接面对的是以下几个方面:
- 需求分析与设计
- 需求分析:明确项目需求,进行用户调研和竞品分析。
- 设计阶段:包括UI设计、交互设计、原型设计等,确保设计稿符合需求和用户体验。
- 开发阶段
- 代码规范:制定统一的代码风格和规范,如ESLint、Prettier等工具的使用。
- 模块化开发:使用模块化开发方式,如CommonJS、ES Modules等,提高代码的可维护性和复用性。
- 版本控制:使用Git进行版本控制,制定合理的分支策略(如Git Flow、GitHub Flow)。
- 构建与部署
- 构建工具:使用Webpack、Rollup等构建工具进行代码打包、压缩、混淆等操作。
- 自动化测试:集成单元测试、集成测试、端到端测试等,确保代码质量。
- 持续集成/持续部署(CI/CD):使用Jenkins、GitLab CI、GitHub Actions等工具实现自动化构建和部署。
- 发布与监控
- 灰度发布:逐步将新版本发布给部分用户,降低风险。
- 监控与日志:使用Sentry、ELK等工具进行错误监控和日志收集,及时发现和解决问题。
脚手架
针对以上方面,我们可以使用脚手架来规范流程,并且提效,所以一个优秀的脚手架可以在以下阶段发力
准备阶段(项目初始化)
- 技术选型;
- 代码规范:
- 分支管理规范;
- 项目初始资源规范;
- UI规范;
- 物料市场规范;
开发阶段
- 开发、打包流程;
- 本地mock服务;
- 代码质量;
- 单元测试&E2E测试;
发布流程
- git commit规范;
- changeLog规范;
- 打包构建;
- 部署、验收;
体验度量
用户体验度量是前端工程化中的重要环节,它帮助开发者了解用户在使用产品时的真实感受,从而优化产品设计和性能。
性能度量
- 页面加载时间:使用Lighthouse、WebPageTest等工具,度量页面的加载时间、首屏渲染时间等。
- 资源加载时间:度量CSS、JS、图片等资源的加载时间,优化资源加载策略。
交互体验度量
- 交互响应时间:度量用户操作(如点击、滚动)的响应时间,优化交互体验。
- 动画流畅度:使用Chrome DevTools等工具,度量动画的帧率,确保动画流畅。
用户行为分析
- 用户行为追踪:使用Google Analytics、Mixpanel等工具,追踪用户行为,分析用户路径和转化率。
- 用户反馈收集:通过问卷调查、用户访谈等方式,收集用户反馈,了解用户需求。
稳定性建设
随着业务迭代的发展,前端(to B/to C端)或多或少都有迭代周期快的压力,在业务的眼里,前端可能更多是“切图仔”,针对前端的具体实现并不关心。导致单人或小团队内很容易造成技术选型自由松散,缺乏约束和专门的技术限制,经常每个人或几个人自己维护一套代码开发流程,技术上更多体现在“拿来主义",工程链路不统一,代码重复度高,页面一致性差,各业务域松散,缺失共享,同时,在代码发布集成后的监控告警几乎没有,缺乏有效的监控手段与快速定位问题(可监控),及时止血(可恢复)的能力,并且缺乏项目的灰度与极值流量的压测,其实以上都是前端稳定性建设需要解决的核心问题。 基于上述内容,总结为三点:
- 可预防;
- 可监控;
- 可回滚; 通过以上三点,我们主要从研发态与运行态出发,通过研发流程的源码框架、工程规范,依赖检测去提高开发质量,发布过程中通过在发布节点上添加监控,做灰度卡口,避免问题带到线上,线上运行时通过实时监控告警实现快速定位问题,快速止血。
稳定性建设流程
可预防
规范团队代码研发流程
通过统一规范前端文档及开发工具,最大可能减少前端研发时差异化部分;
团队文档建设&新人指导 属于软机制,通过文档记录,保证团队在研发基础、故障认知上达成一致;
开发脚手架 通常要支持以下能力:
- git hooks、git commit配置;
- eslint配置;
- 根据命令行配置选择框架template;
- 支持测试用例集成;
组件&物料市场 针对业务属性,梳理常见的开发通用代码,包括但不限于:
- npm包;
- 通用代码snippet集合;
- 业务组件物料市场;
攻防演练
通过日常及大促前的攻防演练,训练面对问题快速止血的演练机制;
故障&压测演练 考察针对流量异常、断网弱网等场景下的降级方案的处理;
代码CR注入 通过在代码code review时加入无效信息,检测是否认真查看CR内容,记录团队攻防数;
灰度方案
CDN分流
- 并不是所有项目都需要灰度发布,在CDN做层拦截对所有项目都有侵扰;
- 根据单一职责,CDN不应该做灰度分流的工作,若用代理模式再CDN前加一层代理分流,实际会造成无效流量的增长;
- CDN要记录用户是否命中灰度,通常需要加cookie,若命中多灰度,cookie增长会过多;
N个版本文件打包到一个文件里
- 灰度比例可以通过随机数比例生产,但是要记录用户是否命中灰度,需要使用localStorage记录;
- 需要将文件*n(n为灰度个数)融合,会造成带宽的浪费;
可监控
错误监控
错误监控是前端监控系统中最基础也是最重要的部分,它帮助开发者实时捕获和分析前端应用中的错误。
JavaScript 运行时错误
使用 window.onerror 或 addEventListener('error') 来捕获全局的 JavaScript 错误。
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:', {message, source, lineno, colno, error});
// 上报错误信息
reportError({
type: 'js',
message,
source,
lineno,
colno,
error: error && error.stack
});
return true;
};
Promise 未捕获异常
使用 unhandledrejection 事件来捕获未处理的 Promise 拒绝。
window.addEventListener('unhandledrejection', function(event) {
console.log('Unhandled Rejection:', event.reason);
// 上报错误信息
reportError({
type: 'promise',
message: event.reason
});
});
资源加载错误
使用 addEventListener('error') 来捕获资源加载错误。
window.addEventListener('error', function(event) {
if (event.target && (event.target.src || event.target.href)) {
console.log('资源加载失败:', event.target.src || event.target.href);
// 上报错误信息
reportError({
type: 'resource',
source: event.target.src || event.target.href
});
}
}, true); // 注意这里的 true,表示在捕获阶段处理
性能监控
页面加载性能
使用 Performance API 来获取页面加载性能指标。
window.addEventListener('load', function() {
setTimeout(function() {
const timing = performance.timing;
const performanceMetrics = {
// DNS 解析时间
dnsTime: timing.domainLookupEnd - timing.domainLookupStart,
// TCP 连接时间
tcpTime: timing.connectEnd - timing.connectStart,
// 首字节时间
ttfb: timing.responseStart - timing.requestStart,
// DOM 加载时间
domTime: timing.domComplete - timing.domLoading,
// 页面加载总时间
loadTime: timing.loadEventEnd - timing.navigationStart
};
console.log('Performance metrics:', performanceMetrics);
// 上报性能指标
reportPerformance(performanceMetrics);
}, 0);
});
网络请求性能
使用 XMLHttpRequest 或 fetch 的拦截器来监控网络请求性能。
// 使用 Proxy 拦截 fetch
const originalFetch = window.fetch;
window.fetch = new Proxy(originalFetch, {
apply: function(target, thisArg, argumentsList) {
const startTime = Date.now();
return target.apply(thisArg, argumentsList).then(response => {
const endTime = Date.now();
const duration = endTime - startTime;
console.log(`Fetch request to ${argumentsList[0]} took ${duration}ms`);
// 上报网络请求性能
reportNetworkPerformance({
url: argumentsList[0],
duration: duration,
status: response.status
});
return response;
});
}
});
首屏渲染时间
使用 Performance API 和 MutationObserver 来估算首屏渲染时间。
const startTime = performance.now();
let firstPaintTime = 0;
const observer = new MutationObserver((mutationsList, observer) => {
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
firstPaintTime = performance.now() - startTime;
observer.disconnect();
console.log('Estimated First Paint Time:', firstPaintTime);
// 上报首屏渲染时间
reportFirstPaint(firstPaintTime);
break;
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
上报机制
使用 Beacon API
Beacon API 允许在页面卸载时也能可靠地发送数据。
function reportError(error) {
const data = JSON.stringify(error);
navigator.sendBeacon('/error', data);
}
function reportPerformance(metrics) {
const data = JSON.stringify(metrics);
navigator.sendBeacon('/performance', data);
}
使用图片请求
对于不支持 Beacon API 的浏览器,可以使用图片请求作为备选方案。
function reportViaImage(url, data) {
new Image().src = `${url}?data=${encodeURIComponent(JSON.stringify(data))}`;
}
批量上报
为了减少网络请求,可以实现一个简单的批量上报机制。
const reportQueue = [];
const BATCH_SIZE = 10;
const REPORT_INTERVAL = 5000; // 5 seconds
function addToReportQueue(data) {
reportQueue.push(data);
if (reportQueue.length >= BATCH_SIZE) {
sendBatchReport();
}
}
function sendBatchReport() {
if (reportQueue.length === 0) return;
const batchData = reportQueue.splice(0, BATCH_SIZE);
navigator.sendBeacon('/batchReport', JSON.stringify(batchData));
}
setInterval(sendBatchReport, REPORT_INTERVAL);
主流监控方案
开源方案:Sentry
Sentry 是一个流行的开源错误跟踪工具,支持多种编程语言和框架。
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
integrations: [new Sentry.BrowserTracing()],
tracesSampleRate: 1.0,
});
// 捕获异常
try {
myUndefinedFunction();
} catch (error) {
Sentry.captureException(error);
}
商业方案:New Relic
New Relic 提供全面的应用性能监控解决方案。
<script type="text/javascript">
window.NREUM||(NREUM={});NREUM.init={privacy:{cookies_enabled:true}};
(window.NREUM||(NREUM={})).loader_config={xpid:"your-xpid-here",licenseKey:"your-license-key-here",applicationID:"your-application-id-here"};
// ... (New Relic 提供的完整脚本)
</script>
自定义监控系统
对于特定需求,可以构建自定义监控系统。
基本架构:
- 前端 SDK:负责收集和上报数据
- 后端 API:接收和存储数据
- 分析系统:处理和可视化数据
前端 SDK 示例:
class MonitorSDK {
constructor(config) {
this.config = config;
this.init();
}
init() {
this.setupErrorCapture();
this.setupPerformanceMonitor();
}
setupErrorCapture() {
window.onerror = (message, source, lineno, colno, error) => {
this.reportError({type: 'js', message, source, lineno, colno, stack: error && error.stack});
};
window.addEventListener('unhandledrejection', (event) => {
this.reportError({type: 'promise', message: event.reason});
});
}
setupPerformanceMonitor() {
window.addEventListener('load', () => {
setTimeout(() => {
const timing = performance.timing;
const performanceMetrics = {
loadTime: timing.loadEventEnd - timing.navigationStart,
// ... 其他性能指标
};
this.reportPerformance(performanceMetrics);
}, 0);
});
}
reportError(error) {
// 使用 Beacon API 上报错误
navigator.sendBeacon(this.config.errorReportUrl, JSON.stringify(error));
}
reportPerformance(metrics) {
// 使用 Beacon API 上报性能指标
navigator.sendBeacon(this.config.performanceReportUrl, JSON.stringify(metrics));
}
}
// 使用
const monitor = new MonitorSDK({
errorReportUrl: '/api/reportError',
performanceReportUrl: '/api/reportPerformance'
});
可回滚
容器化部署
如果将代码和配置分开部署,在回滚的时候就会遇到"应该是先回滚代码还是回滚配置"的难题,所以,要想轻松回滚,在部署的时候,一定要将代码和配置整体打包,这里建议使用容器化部署,保证代码和配置可以整体回滚;
数据迁移
在业务变更涉及数据迁移时,应对数据表的字段采取"只增不删"的原则。因为当某个字段被当前代码引用的字段被删除后,线上业务是会出问题的,但新增一个没有被当前代码引用到的字段,则不会有问题。 等到确认新版代码工作完全正常,不会再回滚到旧版本时,才将旧字段删除。一旦旧字段被删除,引用到旧字段的旧版本代码就无法工作,也就无法回滚到旧版本了。