Skip to content

前端工程化是一个系统工程,涵盖了从需求分析、开发、构建、部署到发布、监控的全流程。通过规范流程、使用脚手架、进行体验度量和稳定性检测,可以显著提高前端开发的效率和质量,确保产品在生产环境中的稳定运行。

规范流程

研发效能

团队能够持续地为用户产生有效价值的效率,包括有效性(Effectiveness)、效率(Efficiency)和可持续性(Sustainability)三个方面。简单来说,就是能否长期、高效地交付出有价值的产品;

对于常规开发阶段而言,我们经常直接面对的是以下几个方面:

  1. 需求分析与设计
  • 需求分析:明确项目需求,进行用户调研和竞品分析。
  • 设计阶段:包括UI设计、交互设计、原型设计等,确保设计稿符合需求和用户体验。
  1. 开发阶段
  • 代码规范:制定统一的代码风格和规范,如ESLint、Prettier等工具的使用。
  • 模块化开发:使用模块化开发方式,如CommonJS、ES Modules等,提高代码的可维护性和复用性。
  • 版本控制:使用Git进行版本控制,制定合理的分支策略(如Git Flow、GitHub Flow)。
  1. 构建与部署
  • 构建工具:使用Webpack、Rollup等构建工具进行代码打包、压缩、混淆等操作。
  • 自动化测试:集成单元测试、集成测试、端到端测试等,确保代码质量。
  • 持续集成/持续部署(CI/CD):使用Jenkins、GitLab CI、GitHub Actions等工具实现自动化构建和部署。
  1. 发布与监控
  • 灰度发布:逐步将新版本发布给部分用户,降低风险。
  • 监控与日志:使用Sentry、ELK等工具进行错误监控和日志收集,及时发现和解决问题。

脚手架

针对以上方面,我们可以使用脚手架来规范流程,并且提效,所以一个优秀的脚手架可以在以下阶段发力

  • 准备阶段(项目初始化)

    1. 技术选型;
    2. 代码规范:
    • 分支管理规范;
    • 项目初始资源规范;
    • UI规范;
    • 物料市场规范;
  • 开发阶段

    1. 开发、打包流程;
    2. 本地mock服务;
    3. 代码质量;
    4. 单元测试&E2E测试;
  • 发布流程

    1. git commit规范;
    2. changeLog规范;
    3. 打包构建;
    4. 部署、验收;

体验度量

用户体验度量是前端工程化中的重要环节,它帮助开发者了解用户在使用产品时的真实感受,从而优化产品设计和性能。

性能度量

  • 页面加载时间:使用Lighthouse、WebPageTest等工具,度量页面的加载时间、首屏渲染时间等。
  • 资源加载时间:度量CSS、JS、图片等资源的加载时间,优化资源加载策略。

交互体验度量

  • 交互响应时间:度量用户操作(如点击、滚动)的响应时间,优化交互体验。
  • 动画流畅度:使用Chrome DevTools等工具,度量动画的帧率,确保动画流畅。

用户行为分析

  • 用户行为追踪:使用Google Analytics、Mixpanel等工具,追踪用户行为,分析用户路径和转化率。
  • 用户反馈收集:通过问卷调查、用户访谈等方式,收集用户反馈,了解用户需求。

稳定性建设

随着业务迭代的发展,前端(to B/to C端)或多或少都有迭代周期快的压力,在业务的眼里,前端可能更多是“切图仔”,针对前端的具体实现并不关心。导致单人或小团队内很容易造成技术选型自由松散,缺乏约束和专门的技术限制,经常每个人或几个人自己维护一套代码开发流程,技术上更多体现在“拿来主义",工程链路不统一,代码重复度高,页面一致性差,各业务域松散,缺失共享,同时,在代码发布集成后的监控告警几乎没有,缺乏有效的监控手段与快速定位问题(可监控),及时止血(可恢复)的能力,并且缺乏项目的灰度与极值流量的压测,其实以上都是前端稳定性建设需要解决的核心问题。 基于上述内容,总结为三点:

  1. 可预防;
  2. 可监控;
  3. 可回滚; 通过以上三点,我们主要从研发态与运行态出发,通过研发流程的源码框架、工程规范,依赖检测去提高开发质量,发布过程中通过在发布节点上添加监控,做灰度卡口,避免问题带到线上,线上运行时通过实时监控告警实现快速定位问题,快速止血。

稳定性建设流程

可预防

规范团队代码研发流程

通过统一规范前端文档及开发工具,最大可能减少前端研发时差异化部分;

  • 团队文档建设&新人指导 属于软机制,通过文档记录,保证团队在研发基础、故障认知上达成一致;

  • 开发脚手架 通常要支持以下能力:

    • git hooks、git commit配置;
    • eslint配置;
    • 根据命令行配置选择框架template;
    • 支持测试用例集成;
  • 组件&物料市场 针对业务属性,梳理常见的开发通用代码,包括但不限于:

  • npm包;
  • 通用代码snippet集合;
  • 业务组件物料市场;

攻防演练

通过日常及大促前的攻防演练,训练面对问题快速止血的演练机制;

  • 故障&压测演练 考察针对流量异常、断网弱网等场景下的降级方案的处理;

  • 代码CR注入 通过在代码code review时加入无效信息,检测是否认真查看CR内容,记录团队攻防数;

灰度方案

  • CDN分流

    1. 并不是所有项目都需要灰度发布,在CDN做层拦截对所有项目都有侵扰;
    2. 根据单一职责,CDN不应该做灰度分流的工作,若用代理模式再CDN前加一层代理分流,实际会造成无效流量的增长;
    3. CDN要记录用户是否命中灰度,通常需要加cookie,若命中多灰度,cookie增长会过多;
  • N个版本文件打包到一个文件里

    1. 灰度比例可以通过随机数比例生产,但是要记录用户是否命中灰度,需要使用localStorage记录;
    2. 需要将文件*n(n为灰度个数)融合,会造成带宽的浪费;

可监控

错误监控

错误监控是前端监控系统中最基础也是最重要的部分,它帮助开发者实时捕获和分析前端应用中的错误。

JavaScript 运行时错误

使用 window.onerror 或 addEventListener('error') 来捕获全局的 JavaScript 错误。

js
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 拒绝。

js
window.addEventListener('unhandledrejection', function(event) {
    console.log('Unhandled Rejection:', event.reason);
    // 上报错误信息
    reportError({
        type: 'promise',
        message: event.reason
    });
});
资源加载错误

使用 addEventListener('error') 来捕获资源加载错误。

js
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 来获取页面加载性能指标。

js
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 的拦截器来监控网络请求性能。

js
// 使用 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 来估算首屏渲染时间。

js
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 允许在页面卸载时也能可靠地发送数据。

js
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 的浏览器,可以使用图片请求作为备选方案。

js
function reportViaImage(url, data) {
    new Image().src = `${url}?data=${encodeURIComponent(JSON.stringify(data))}`;
}
批量上报

为了减少网络请求,可以实现一个简单的批量上报机制。

js
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 是一个流行的开源错误跟踪工具,支持多种编程语言和框架。

js
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 提供全面的应用性能监控解决方案。

html
<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>
自定义监控系统

对于特定需求,可以构建自定义监控系统。

基本架构:

  1. 前端 SDK:负责收集和上报数据
  2. 后端 API:接收和存储数据
  3. 分析系统:处理和可视化数据

前端 SDK 示例:

js
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'
});

可回滚

容器化部署

如果将代码和配置分开部署,在回滚的时候就会遇到"应该是先回滚代码还是回滚配置"的难题,所以,要想轻松回滚,在部署的时候,一定要将代码和配置整体打包,这里建议使用容器化部署,保证代码和配置可以整体回滚;

数据迁移

在业务变更涉及数据迁移时,应对数据表的字段采取"只增不删"的原则。因为当某个字段被当前代码引用的字段被删除后,线上业务是会出问题的,但新增一个没有被当前代码引用到的字段,则不会有问题。 等到确认新版代码工作完全正常,不会再回滚到旧版本时,才将旧字段删除。一旦旧字段被删除,引用到旧字段的旧版本代码就无法工作,也就无法回滚到旧版本了。