前端工程化实践总结 | QQ音乐商业化Web团队

| 导语 本文主要介绍在前端工程化的一些探索和实践,结合移动端的基础库重构和UI组件库开发这两个项目详细介绍工程化方案 。

随着业务的不断扩展,团队的项目越来越多,面对日益复杂的业务场景和代码逻辑,我们发现在前端工程化方面团队还有很多需要优化的地方。现有的解决方案已经无法满足各种复杂的场景,我们每天都在疲于应付很多重复的工作,为此我们基于移动端基础库重构和UI组件库的建设这两个项目对团队的项目构建流程进行了详细的分析和梳理,并制定了一套适用于团队的工程化方案。

浅谈前端工程化

前端工程化是一个非常广泛的议题,包含的技术和解决方案也是非常丰富的。一个前端工程的生命周期可以大致划分为这四个过程:

前端工程的生命周期

任何在这四个过程中应用的系统化、严格约束、可量化的方法都可以称之为工程化。工程化的程度越高,在工作中因人的个体差异性导致的缺陷或者短板就会越少,项目质量可以得到更有效的保障。对上面四个过程的工程化并不是完全分隔的,而是相辅相成,比如开发阶段的优化也会对测试、部署和维护产生很大的影响。

下面从模块化、组件化、规范化和自动化这四个方面进行具体介绍。


模块化

模块化可以对复杂逻辑进行有效分割,每个模块更关注自身的功能,模块内部的数据和实现是私有的,通过向外部暴露一些接口来实现各模块间的通信。开发阶段前端需要关注JS、CSS和HTML,下面我们将分别对JS、CSS、HTML的模块化进行简单介绍。


1. JS模块化

JS模块化是一个逐渐演变的过程,开始的namespace概念实现了简单对象封装,约定私有属性使用_开头,到后来的IIFE模式,利用匿名函数闭包的原理解决模块的隔离与引用,下面介绍现在比较流行的几种模块化标准。


2. CommonJS

Nodejs中的模块化方案,就是基于CommonJS规范实现的。一个文件就是一个模块,有自己的作用域,没有export的变量和方法都是私有的,不会污染全局作用域,模块的加载是运行时同步加载的。CommonJS可以细分为CommonJS1和CommonJS2,二者的模块导出方式不同,CommonJS2兼容CommonJS1,增加了module.exports的导出方式,现在一般所指的都是CommonJS2。

  • 每个文件一个模块,有自己的作用域,不会污染全局;

  • 使用require同步加载依赖的其他模块,通过module.exports导出需要暴露的接口;

  • 多次require的同一模块只会在第一次加载时运行,并将运行结果缓存,后续直接读取缓存结果,如果需要重新执行,需要先清理缓存;

  • Nodejs环境下可以直接运行,各个模块按引入顺序依次执行。

module.exports.add = function (a, b) {    return a + b;}
exports.add = function (a, b) { return a + b;}

const sum = require('sum');sum.add(1, 2);
AMD

浏览器加载js文件需要进行网络请求,而网络请求的耗时是不可预期的,这使得CommonJS同步加载模块的机制在浏览器端并不适用,我们不能因为要加载某个模块js而一直阻塞浏览器继续执行下面的代码。AMD规范则采用异步的方式加载模块,允许指定回调函数,这非常适合用于浏览器端的模块化场景。

  • 使用define定义一个模块,使用require加载模块;

  • 异步加载,可以并行请求依赖模块;

  • 原生JavaScript运行环境无法直接执行AMD规范的模块代码,需要引入第三方库支持,如requirejs等;

// 定义一个模块define(id ? , dependencies ? , factory); // 引用一个模块require([module], callback)
CMD

类似于AMD规范,是应用在浏览器端的JS模块化方案,由sea.js提出,详见 https://www.zhihu.com/question/20351507 。


UMD

UMD规范兼容AMD和CommonJS,在浏览器和Nodejs中均可以运行。

(function (root, factory) {    if (typeof define === 'function' && define.amd) {                define(['jquery', 'underscore'], factory);    } else if (typeof exports === 'object') {                module.exports = factory(require('jquery'), require('underscore'));    } else {                root.returnExports = factory(root.jQuery, root._);    }}(this, function ($, _) {        function a() {};    function b() {};    function c() {};
return { b: b, c: c }}));
ES6 Module

ES6从语言标准的层面上实现了模块化,是ECMA提出的模块化标准,后续浏览器和Nodejs都宣布会原生支持,越来越受开发者青睐。

  • 使用import引入模块,export导出模块;

  • 与CommonJS的执行时机不同,只是个只读引用,只会在真正调用的地方开始执行,而不是像CommonJS那样,在require的时候就会执行代码;

  • 支持度暂不完善,需要进行代码转换成上面介绍的某一种模块化规范。

在浏览器中可以通过下面的方式引入es6规范的模块js:

<script type="module" src="foo.mjs"></script> <script type="module" src="foo.mjs" defer></script>

defer和async不同,它会阻塞DomContentLoaded事件,每个模块js会根据引入的顺序依次执行。

随着更多浏览器对ES6的支持,现在有一些方案开始提出直接使用ES2015+的代码在浏览器中直接执行来提高运行效果,这篇文章《Deploying ES2015+ Code in Production Today》中有详细的介绍,可以结合这份性能测试报告综合评估ES6在node以及各种浏览器环境下的执行效率对比。


3. CSS模块化

CSS 自诞生以来,基本语法和核心机制一直没有本质上的变化,它的发展几乎全是表现力层面上的提升。不同于JS,CSS本身不具有高级编程属性,无法使用变量、运算、函数等,无法管理依赖,全局作用域使得在编写CSS样式的时候需要更多人工去处理优先级的问题,样式名还有压缩极限的问题,为此,出现了很多“编译工具”和“开发方案”为CSS赋予“编程能力”。


预处理器

随着页面越来越复杂,为了便于开发和维护,我们常常会将CSS文件进行切分,然后再将需要的文件进行合并。诸如LESS、SASS、Stylus等预处理器为CSS带来了编程能力,我们可以使用变量、运算、函数,@import指令可以轻松合并文件。但各种预处理器并不能完全解决全局作用域的问题,需要结合namespace的思想去命名。


OOCSS & SMACSS

OOCSS和SMACSS都是有关css的方法论。OOCSS(Object Oriented CSS)即面向对象的CSS,旨在编写高可复用、低耦合和高扩展的CSS代码,有两个主要原则,它们都是用来规定应该把什么属性定义在什么样式类中。

  • Separate structure and skin(分离结构和主题)

  • Separate container and content(分离容器和内容)

SMACSS(Scalable and Modular Architecture for CSS)是可扩展模块化的CSS,它的核心就是结构化CSS代码,则有三个主要规则:

  • Categorizing CSS Rules (CSS分类规则):将CSS分成Base、Layout、Module、State、Theme这5类。

  • Naming Rules(命名规则):考虑用命名体现样式对应的类别,如layout-这样的前缀。

  • Minimizing the Depth of Applicability(最小化适配深度):降低对特定html结构的依赖。

/* 依赖html结构,不提倡 */.sidebar ul h3 { }
/* 建议直接定义 */.sub-title { }
BEM

BEM是一种CSS命名规范,旨在解决样式名的全局冲突问题。BEM是块(block)、元素(element)、修饰符(modifier)的简写,我们常用这三个实体开发组件。

  • 块(block):一种布局或者设计上的抽象,每一个块拥有一个命名空间(前缀)。

  • 元素(element):是.block的后代,和块一起形成一个完整的实体。

  • 修饰符(modifier):代表一个块的状态,表示它持有的一个特定属性。

在选择器中,BEM要求只使用类名,不允许使用id,由以下三种符号来表示扩展的关系:

  • 中划线( - ) :仅作为连字符使用,表示某个块或者某个子元素的多单词之间的连接记号。

  • 双下划线( __ ):双下划线用来连接块和块的子元素。

  • 单下划线( _ ):单下划线用来描述一个块或者块的子元素的一种状态。

.type-block__element_modifier {}

从上面BEM的命名要求可以看到,类名都很长,这就导致在对CSS文件进行压缩的时候,我们无法得到更大的优化空间。而且BEM仅仅是一种规范,需要团队中的开发者自行遵守,在可靠性上无法得到有效保障,而且还可能和第三方库的命名冲突。


CSS in JS

CSS in JS是一种比较激进的方案,彻底抛弃了CSS,完全使用JS来编写CSS,又用起了行内样式(inline style),它的发展得益于React的出现,具体的原因可以参见组件化这部分内容。

  • 解决全局命名污染的问题;

  • 更贴近Web组件化的思想;

  • 可以在一些无法解析CSS的运行环境下执行,比如React Native等;

  • JS赋予CSS更多的编程能力,实现了CSS和JS间的变量共享;

  • 支持CSS单元测试,提高CSS的安全性;

  • 原生JS编写CSS无法支持到很多特性,比如伪类、media query等,需要引入额外的第三方库来支持,各种库的对比详见css-in-js;

  • 有运行时损耗,性能比直接class要差一些;

  • 不容易debug;

下面以styled-components为例:

import React from 'react';import styled from 'styled-components';
const Container = styled.div` text-align: center;`;

const App = () => ( <Container> It is a test! </Container>);
render(<App />, document.getElementById('content'));

构建后的结果如下,我们发现不会再有.css文件,一个.js文件包含了组件相关的全部代码:

var _templateObject = _taggedTemplateLiteral(['\n  text-align: center;\n'], ['\n  text-align: center;\n']);
function _taggedTemplateLiteral(strings, raw) { return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } }));}
var Container = _styledComponents2.default.div(_templateObject);
var App = function App() { return _react2.default.createElement( Container, null, 'It is a test!' );};
CSS module

CSS module则最大化地结合了现有CSS生态和JS模块化的能力,以前用于CSS的技术都可以继续使用。CSS module最终会构建出两个文件:一个.css文件和一个.js。

  • 解决全局命名污染的问题;

  • 默认是局部的,可以用:global声明全局样式;

  • 受CSS的限制,只能一层嵌套,和JS无法共享变量;

  • 能支持现在所有的CSS技术。

以webpack为例,使用css-loader就可以实现CSS module:

module.exports = {    ...    module: {        rules: [            ...            {                loader: 'css-loader',                options: {                    importLoaders: 1,                    modules: {                        localIdentName: "[name]__[local]--[hash:base64:5]"                    },
} } ... ] } ...}

下面是一个组件开发的例子:

/* style.css */.color {    color: green;}
:local .className .subClass :global(.global-class-name) { color: blue;}
/* component.js */import styles from './style.css';elem.outerHTML = `<h1 class=${styles.color}>It is a test title</h1>`;

构建运行后生成的dom结构如下:

<h1 class="style__color--rUMvq">It is a test title</h1>

component.js中styles变量的值如下,我们看到声明成:global的类名.global-class-name没有被转换,具有全局作用域。

const styles = {    "color": "style__color--rUMvq",    "className": "style__className--3n_7c",    "subClass": "style__subClass--1lYnt"}

说明:React对样式如何定义并没有明确态度,无论是BEM规范,还是CSS in JS或者CSS module都是支持的,选择何种方案是开发者自行决定的。

组件化

最初,网页开发一般都会遵循一个原则”关注点分离”,各个技术只负责自己的领域,不能混合在一起,形成耦合。HTML只负责结构,CSS负责样式,JS负责逻辑和交互,三者完全隔离,不提倡写行内样式(inline style)和行内脚本(inline script)。React的出现打破了这种原则,它的考虑维度变成了一个组件,要求把组件相关的HTML、CSS和JS写在一起,这种思想可以很好地解决隔离的问题,每个组件相关的代码都在一起,便于维护和管理。

我们回想一下原有引用组件的步骤:

  1. 引入这个组件的JS;

  2. 引入这个组件的样式CSS(如果有);

  3. 在页面中引入这个组件的;

  4. 最后是编写初始化组件的代码。

这种引入方式很繁琐,一个组件的代码分布在多个文件里面,而且作用域暴露在全局,缺乏内聚性容易产生冲突。

组件化就是将页面进行模块拆分,将某一部分独立出来,多个组件可以自由组合形成一个更复杂的组件。组件将数据、视图和逻辑封装起来,仅仅暴露出需要的接口和属性,第三方可以完全黑盒调用,不需要去关注组件内部的实现,很大程度上降低了系统各个功能的耦合性,并且提高了功能内部的聚合性。


1.React、Vue、Angular…

React、Vue、Angular等框架的流行推动了Web组件化的进程。它们都是数据驱动型,不同于DOM操作是碎片的命令式,它允许将两个组件通过声明式编程建立内在联系。

<!-- 数据驱动的声明式Declarative--><pagination     current={current} total={maxCount/20}     on-nav={this.nav(1)}></pagination>
<!-- DOM操作的命令式Imprective --><pagination id='pagination'></pagination><script>// 获取元素var pagination = document.querySelector('#pagination');
// 绑定事件pagination.addEventListener('pagination-nav', function(event){ ...})
// 设置属性$.ajax('/blogs').then(function( json ){ pagination.setAttribute('current', 0) pagination.setAttribute('total', json.length / 20)})</script>

从上面的例子可以看到,声明式编程让组件更简单了,我们不需要去记住各种DOM相关的API,这些全部交给框架来实现,开发者仅仅需要声明每个组件“想要画成什么样子”。

  • JSX vs 模板DSL

React使用JSX,非常灵活,与JS的作用域一致。Vue、Angular采用模板DSL,可编程性受到限制,作用域和JS是隔离的,但也是这个缺点使得我们可以在构建期间对模板做更多的事情,比如静态分析、更好地代码检查、性能优化等等。二者都没有浏览器原生支持,需要经过Transform才能运行。


2.Web Component

Web Component是W3C专门为组件化创建的标准,一些Shadow DOM等特性将彻底的、从浏览器的层面解决掉一些作用域的问题,而且写法一致,它有几个概念:

  • Custom Element: 带有特定行为且用户自命名的 HTML 元素,扩展HTML语义;

<x-foo>Custom Element</x-foo>
/* 定义新元素 */var XFooProto = Object.create(HTMLElement.prototype);
// 生命周期相关XFooProto.readyCallback = function() { this.textContent = "I'm an x-foo!";};
// 设置 JS 方法XFooProto.foo = function() { alert('foo() called'); };
var XFoo = document.register('x-foo', { prototype: XFooProto });
// 创建元素var xFoo = document.createElement('x-foo');
  • Shadow DOM:对标签和样式的一层 DOM 封装,可以实现局部作用域;当设置{mode: closed}后,只有其宿主才可定义其表现,外部的api是无法获取到Shadow DOM中的任何内容,宿主的内容会被Shadow DOM掩盖。

var host = document.getElementById('js_host');var shadow = host.attachShadow({mode: 'closed'});shadow.innerHTML = '<p>Hello World</p>';

Chrome调试工具:DevTool > Settings > Preferences> Show user agent shadow DOM

Chrome调试工具查看shadow DOM 

HTML Template & Slots: 可复用的 HTML 标签,提供了和用户自定义标签相结合的接口,提高组件的灵活性。定义了template的标签,类似我们经常用的<script type='tpl'>,它不会被解析为dom树的一部分,template的内容可以被塞入到Shadow DOM中并且反复使用;template中定义的style只对该template有效,实现了隔离。

<template id="tpl">    <style>       p {         color:red;       }</style>     <p>hello world</p></template>
<script> var host = document.getElementById('js_host'); var shadow = host.attachShadow({mode: 'open'}); var tpl = document.getElementById("tpl").content.cloneNode(true); shadow.appendChild(tpl);</script>

dom树中的template标签,不解析:

HTML template-1 

最终插入的影子节点效果:

HTML template-2 

由于Shadow DOM中宿主元素的内容会被影子节点掩盖,如果想将宿主中某些内容显示出来的话就需要借助slot,它是定义在宿主和template中的一个插槽,用来“占位”。

<div id="host">    <span>Test1</span>    <span slot="s1">slot1</span>    <span slot="s2">slot2</span>    <span>Test2</span></div><template id="tpl">    <span>tpl1</span>    <slot name="s1"></slot>    <slot name="s2"></slot>    <span>tpl2</span></template>

宿主元素中设置了slot属性的节点被“保留”了下来,并且插入到了template中定义的slot的位置。

 slot的示例 

  • HTML Imports: 打包机制,将HTML代码以及Web Componnet导入到页面中,这个规范目前已经不怎么推动了,在参考了ES6 module的机制后,FireFox团队已经不打算继续支持。

<link rel="import" href="/path/to/imports/stuff.html">
  • Polymer

Polymer是基于Web Componet的一种数据驱动型开发框架,可以使用ES6 class来定义一个Web Component,由于现在浏览器对Web Component的支持度还不是很好,需要引入一些polyfill才能使用。

React和Web Component并不是对立的,它们解决组件化的角度是不同,二者可以相互补充。与Web Component不同的是React中的HTML标签运行在Virtual DOM中,在非标准的浏览器环境,React的这种机制可以更好地实现跨平台,Web Component则更有可能实现浏览器大统一,是浏览器端更彻底的一种解决方案。


规范化

规范化是保障项目质量的一个重要环节,可以很好地降低团队中个体的差异性。


1.代码规范

代码规范是一个老生常谈的话题,我们需要制定一些原则来统一代码风格,虽然不遵守规范的代码也是可以运行的,但是这会对代码的维护带来很多麻烦。


Lint

根据维基百科的介绍,首先看一下lint的定义:

lint最初是一个特定程序的名称,它在C语言源代码中标记了一些可疑的和不可移植的构造(可能是bug)。这个术语(lint或者linter)现在一般用于称呼那些可以标记任何计算机语言编写的软件中可疑用法的工具,这些工具通常执行源代码的静态分析。

一般代码的Linter工具提供下面两大类的规则:

  • 格式化规则:比如 max-len, no-mixed-spaces-and-tabs等等,这些规则只是用来统一书写格式的。

  • 代码质量规则:比如 no-unused-vars, no-extra-bind, no-implicit-globals等等,这些规则可以帮助提升代码质量,减少bug。

在实际的项目中可以引入lint的机制来提升代码质量,可以参考GitHub 官方出品的 Lint 工具列表 ,下面简单介绍几个常用工具。

Prettier

Prettier是一个代码格式化工具,可以统一团队中的书写风格,比下面Eslint这类工具的功能要弱,因为只是对格式上的约束,无法对代码质量进行检测。

ESlint

ESLint是一款非常常用的JS编程规范库,当然还有很多其他的lint工具。下面的表格里简单介绍了3种常用的规范标准,可以在ESLint中配置选择哪一种标准,每一种标准都会包含很多编程规则。各个标准没有绝对的孰优孰劣,选择适用于团队的编程风格和规范就好。

标准简介
Airbnb JavaScript Style Guide目前最受欢迎的JS编程规范之一,对很多JS框架都有支持,比如React等。
Google JavaScript Style GuideGoogle Style的JS编程规范。
JavaScript Standard Style Guide很强大,自带linter和自动代码纠正,无需配置,自动格式化代码。很多知名公司所采用,比如 Nodejs、npm、express、GitHub、mongoDB 等。

husky

如果我们把Lint放在了持续集成CI阶段,就会遇到这样一个问题:CI系统在Lint时发现了问题导致构建失败,这个时候我们需要根据错误重新修改代码,然后重复这个过程直到Lint成功,整个过程可能会浪费掉不少时间。针对这个问题,我们发现只在CI阶段做Lint是不够的,需要把Lint提前到本地来缩短整个修改链路。但是将Lint放在本地仅仅依靠开发者的自觉遵守是不够的,我们需要更好的方案,需要依靠流程来保障而不是人的自觉性。

Lint的问题 

husky可以注册git hooks,拦截一些错误的提交,比如我们就可以在pre-commit这个hook中增加Lint的校验,这里可以查看支持的git hooks。

lint-staged

通过husky注册的git hook会对仓库中的全部文件都执行设置的npm命令,但我们仅仅需要对提交到staged区的文件进行处理来减少校验时间,lint-staged可以结合husky实现这个功能,在package.json中的示例:

{  "husky": {      "hooks": {          "pre-commit": "lint-staged",      }  },  "lint-staged": {    "src/**/*.js": "eslint"  }}

类型检查

JavaScript是非常灵活的,这得益于它的弱类型语言特点,但也是因为这个原因,我们只有在运行时才知道变量到底是什么类型,无法在编译阶段作出任何类型错误的提示,同时由于函数参数类型的不确定性,编译器的编译结果很可能无法被复用,比如下面的例子中,在执行add(1,2)时对add函数的编译结果无法直接被下面的add('1', '2')复用,第二次调用必须得再重新编译一次,这对性能也是有很大影响。

function add(a, b) {    return a + b;}add(1, 2);add('1', '2');

类型检查可以让我们编写出更高质量的代码,减少类型错误的bug,同时明确了类型也让代码更好维护。

PropTypes

React在15.5的版本后将类型检查React.PropTypes移除后使用prop-types库代替,它是一种运行时的类型检测机制,包含一整套验证器,可用于确保组件属性接收的数据是正确的类型。

import React, { Component } from 'react';import PropTypes from 'prop-types';
class App extends Component {
}
App.propTypes = { title: PropTypes.string.isRequired}

Flow

和PropTypes不同,Flow是一种静态类型检查器,由Facebook开源,赋予JS强类型的能力,在编译阶段就可以检测出是否有类型错误,可以被用于任何JavaScript项目。

Flow主要有两个工作方式:

  • 类型推断:通过变量的使用上下文来推断出变量类型,然后根据这些推断来检查类型。

  • 类型注释:事先注释好我们期待的类型,Flow会基于这些注释来判断。

function split(str) {      return str.split(' ')}split(11);
function square(n: number): number { return n * n;}square("2");

Flow风格的代码不能直接在JS运行环境中执行,需要使用babel进行转换。就目前的发展和生态而言,Flow离TypeScript的差距已经越来越遥远了,Vue在2.0版本开始使用flow.js,但从3.0起已经替换成了TypeScript。

TypeScript

TypeScript则是一种JavaScript语言的超集,强类型、支持静态类型检查,更像是一门“新语言”。Deno已经支持直接运行tcs了,不需要进行转换。


interface Person {    firstName: string;    lastName: string;}
function greeter(person: Person) { return "Hello, " + person.firstName + " " + person.lastName;}

2.文档规范

高质量的项目文档可以提高团队协作效率,便于后期优化维护。维护文档很重要,但是也很繁琐,我们经常会看到代码和文档南辕北辙互相矛盾,下面介绍几种文档构建工具,它们可以很好地帮助我们构建文档,而对于React、Vue等组件而言,使用MDX可以非常便捷构建demo,极大减少人工保证代码和文档一致性的工作:

  • JSDoc:根据.js文件中的注释信息,生成API文档。

  • Docz:基于MDX的高效、零配置的文档生成工具,目前仅支持React。

  • Storybook:集组件开发、查看、测试的文档工具,支持React、RN、Vue、Angular、Polymer等很多框架,非常强大。

  • react-styleguidist:和Storybook类似,生成React组件开发环境的文档服务,基于webpack支持HRM。

3.流程规范

当团队在开发时,通常会使用版本控制系统来管理项目,常用的有svn和git,如何合并代码、如何发布版本都需要相应的流程规范,这可以让我们规避很多问题,比如合并代码后出现代码丢失,又或者将别人未经测试的代码发布出去等等。下面主要介绍几种基于git的协作开发模式:


github-flow

以部署为中心的开发模式,持续且高速安全地进行部署,具体流程如下:

  1. master分支一直是可部署的状态,这意味着不要直接在master分支上进行push操作;

  2. 每次开发都从master分支创建一个新的特性分支,命名需要有含义;

  3. 在远端创建对应的origin/特性分支,定期push;

  4. 开发测试完毕后需要merge的时候,创建Pull Request进行交流;

  5. 其他开发者review这次Pull Request,确认后与master分支进行合并。

  6. 立刻部署合并后的master分支代码,删除该分支。

github-flow的最大特点就是简单,只有一个master长期分支,但是由于要持续部署,当一个部署还未完成的时候,往往下一个Pull Request已经完成,这就导致在开发速度越来越快的时候,必须要让部署所需的一系列流程都是自动化的,比如有自动化测试、接入CI等。


git-flow

有两个长期分支master和develop,这意味着不要直接在这两个分支上进行push操作,所有的开发都在feature分支上进行,详见文档。

git-flow工作流 

功能开发:首先从develop分支创建feature分支,然后和上面github-flow的流程类似,开发测试完毕后向develop分支发起Pull Request,其他开发者review完毕后将此次PR合并至develop分支。

管理Release:当develop分支可以release的时候,首先创建一个release/版本号分支,然后对这个release分支打上tag后再合并到develop和master中去。

hotfix:当出现了紧急bug的时候,需要开启“hotfix”流程,和release不同的是,这个hotfix分支是基于master创建的,修复bug后提交到这个hotfix分支,然后又和release分支的处理非常类似,改动会被同时合并到develop和master中去,最后这个hotfix分支被删除掉。


gitlab-flow

github-flow有一个问题,它要求master分支和生产环境是完全一致,一旦PR通过被合并到了master分支,就要立刻部署发布到生成环境,但是往往受限于产品发布时间,master分支很可能会先于生产环境,这个时候不能依靠master分支来追踪线上版本。

git-flow的流程比较复杂,需要维护两个长期分支master和develop,开发过程要以develop分支为准,但是很多开发工具将master当做默认分支,就需要频繁切换分支。git-flow的模式是基于“版本发布”,这对一些持续发布部署的项目不太适用。

gitlab-flow则是上面两个工作流的综合,推出一个“上游优先”的最大原则,即只存在一个master主分支,它是所有分支的上游,只有合并到master上的代码才能应用到其他分支,详见文档。

持续发布

对于这种模式的项目,master分支对应开发环境,然后需要再创建pre-production和production两个分支,它们的上游链路依次是:master分支—>pre-production分支—>production分支,只有合并进入master分支的代码修改才能依次应用合并到”下游”。

版本发布

在这种模式下,首先基于master分支创建某个版本的stable分支,然后将代码改动合并进master分支,当需要发版本的时候,将master分支使用cherry-pick合并到stable分支中去,然后基于stable分支进行项目的发布部署。


自动化

自动化是前端工程化的一个重要组成部分,可以减少重复的工作,提高工作效率。


1.构建

在前端项目开发中我们使用了模块化的方案,有可能还引入了组件化的机制,依赖一些开发框架,这个时候就需要对项目进行构建,构建一般可以包括这几个步骤:

代码转换:允许使用更高级的JavaScript语法,比如ES6、TypeScript等等,这对代码的开发和可维护性来说是非常有好处的。

模块合并:按模块化开发的代码需要进行打包合并。

文件优化:常见的有代码压缩和Code Splitting,使用ES6 module的模块化机制的还可以考虑构建工具的Tree Shaking功能,进一步减少代码体积。

自动刷新:在开发过程中支持file watch和HMR都是可以很好地提升开发效率。


2.测试

在软件的生命周期中,不同的测试阶段,针对的测试问题是不一样的:

  • 单元测试:确保每个组件/模块正常工作

  • 集成测试:在单元测试的基础上,确保组装成模块、子系统或系统的过程中各部分正常合作

  • 系统测试:在集成测试的基础上,确保整个应用运行正常

  • 验收测试:也称交付测试,是针对用户需求、业务流程进行的正式的测试,以保证达到验收标准

JavaScript 单元测试,我们真的需要吗?答案是需要结合项目看实际情况。如果是基础库或者公共组件这样的项目,单元测试还是很有必要的。而对于那种就上线几天的活动页,写详细的单元测试可能真的会有点入不敷出。引用这篇文章结尾处是思考:

“怎么单元测试写起来这么麻烦”
——说明项目模块之间存在耦合度高,依赖性强的问题。
“怎么要写这么长的测试代码啊”
——这是一劳永逸的,并且每次需求变更后,你都可通过单元测试来验证,逻辑代码是否依旧正确。
“我的模块没问题的,是你的模块出了问题”
——程序中每一项功能我们都用测试来验证的它的正确性,快速定位出现问题的某一环。
“上次修复的 bug 怎么又出现了 ”
——单元测试能够避免代码出现回归,编写完成后,可快速运行测试。

TDD (测试驱动开发Test-Driven Development)和 BDD (行为驱动开发Behavior Driven Development)是两种开发模式,并不是单单指如何进行代码测试,它们定义了一种软件开发模式来将用户需求、开发人员和测试人员进行有效的联合,减少三者之间的脱节。TDD要求开发者先写测试用例,然后根据测试用例的结果再写真正实现功能的代码,接下来继续运行测试用例,再根据结果修复代码,该过程重复多次,直到每个测试用例运行正确。BDD则是对TDD的一种补充,我们无法保证在TDD中的测试用例可以完全达到用户的期望,那么BDD就以用户期望为依据,从用户的需求出发,强调系统行为。具体区别可以详见文章The Difference Between TDD and BDD。

前端如何做单元测试?

  • 测试环境

和后端不同,前端有运行环境的差异性,需要考虑兼容性,如何模拟浏览器环境,如何支持到BOM API的调用,这些都是需要考虑的。可以考虑以下几种测试环境的解决方案:

运行环境特点

jsdom

node端直接运行,伪浏览器环境,速度快,内置BOM对象,目前也有了对sessionStorage、localStorage和cookie的支持。

puppeteer

在真实的浏览器中运行测试,很方便,但是运行速度会慢一点。

phantomjs

无头浏览器,在puppeteer发布后,作者已经宣布不维护了。

  • 测试工具

测试框架就是运行测试用例的工具,常见的有Macha、Jasmine、Jest、AVA等等。

断言库主要提供语义化方法,用于对参与测试的值做各种各样的判断。这些语义化方法会返回测试的结果,要么成功、要么失败。Node内置断言库assert,常见的断言库还有有chai.js、should.js。断言库可以支持不同的开发模式,比如chai.js就是一个BDD/TDD模式的断言库。

测试覆盖率工具是用于统计测试用例对代码的测试情况,生成相应的报表,如Istanbul(Jest内置集成)。

Karma是一个测试平台,可以在多种真实浏览器(e.g Chrome Firefox Safari IE等等)中运行JavaScript代码,可以和很多测试框架集成,比如Mocha、Jasmine等等,还可以使用Istanbul自动生成覆盖率报告。


CI/CD

首先先看一张图片,来理解Agile(敏捷开发)、CI(持续集成),CD(持续交付/部署)和DevOps(开发运维一体化)涵盖的生命周期范围。CI/CD并不等同于DevOps,它们只是DevOps的部分流程中的一种解决方案。

DevOps是Development和Operations的组合,是一种方法论,是一组过程、方法与系统的统称,用于促进应用开发、应用运维和质量保障(QA)部门之间的沟通、协作与整合。以期打破传统开发和运营之间的壁垒和鸿沟。

 

各个术语涵盖的生命周期范围 

持续集成(Continuous Integration)中开发人员需要频繁地向主干提交代码,这些新提交的代码在最终合并到主干前,需要经过编译和自动化测试(通常是单元测试)进行验证。

CI的好处在于可以防止分支偏离主干太久,这种持续集成可以实现产品快速迭代,但是由于要频繁集成,所以需要支持自动化构建、代码检查和测试,实现这些自动化流程是CI的核心。

持续集成 

持续交付(Continuous Delivery)指的是,频繁地将软件的新版本,交付给质量团队或者用户,以供评审。如果评审通过,代码就进入生产阶段。

CD是CI的下一步,它的目标是拥有一个可随时部署到生产环境的代码库。

 持续交付 

持续部署是持续交付的延伸,实现自动将应用发布到生产环境。

 

持续部署 

公司内部常用的解决方案有:蓝盾DevOps平台 、orange-ci、QCI,各花入各眼,详情可以阅读这篇文章CI工具哪家强。

这些CI平台是怎样将git仓库中的代码变动和自动化构建流程相关联起来的呢?答案就是Webhook,它与异步编程中“订阅-发布模型”非常类似,一端触发事件,一端监听执行。

在web开发过程中的Webhook,是一种通过通常的callback,去增加或者改变web page或者web app行为的方法。这些callback可以由第三方用户和开发者维持当前,修改,管理,而这些使用者与网站或者应用的原始开发没有关联。Webhook这个词是由Jeff Lindsay在2007年在计算机科学hook项目第一次提出的。

  • Webhooks是”user-defined HTTP回调”。它们通常由一些事件触发,这里可以查看GitHub上面支持的Event类型,比如git push、fork等等,也就是说这些代码托管平台首先要支持Webhook的功能。

  • 当事件发生时,源网站可以发起一个HTTP请求到Webhook配置的URL。通常这里配置的URL指向某个CI系统,这意味着当git仓库中“订阅”的事件发生时,CI系统可以收到通知。

  • CI系统在收到通知后就可以触发build等流程。

CI自动化构建只是应用Webhook的一个案例,Webhook的应用远不止这些,由于webhook使用HTTP协议,因此可以直接被集成到web service,有时会被用来构建消息队列服务,例如一些RESTful的例子:IronMQ和RestMS。

我们的项目构建现状

介绍完了前端工程化的一些概念和技术后,下面结合我们团队中的具体项目具体分析。

1.现状分析

这是目前团队移动端基础库的项目结构:主要有9个模块,其中3个UI组件依赖框架。

基础库项目结构 

  • 模块化

我们团队在移动端基础库的开发中,最初采用的是IIFE模式。从严格意义上来说,这并不是一种标准的模块化方式,只是通过闭包实现了私有数据,将数据和行为封装到一个函数内部, 通过给全局对象window.M添加属性来向外暴露接口,我们无法确认每个模块间的依赖关系,模块合并时还要关注依赖顺序。在新的方案中,我们引入了ES6的模块化标准来解决这个问题。

  • 重复开发,复制粘贴

由于业务特点,对于一些快速上线的活动页使用Zepto库,而对常驻页面进行了技术升级,社交团队使用了Preact框架,这导致基础库的开发有了两个版本,分别在不同的代码仓库维护,但实际上二者90%+的代码都是一样的,仅仅是三个UI组件不同。在基于TSW的同构直出项目中,有些基础库方法又要在node端执行,这个时候也是复制粘贴了一份m.js放到了该项目目录中。在新的方案中,我们使用差异化的构建在一份代码仓库中分别构建出多个版本。

  • 组件css的问题

对于组件的样式,我们是有专门的重构组进行开发维护的,他们遵循BEM规范,开发组件的时候当字符串引入:

var css ='.qui_dialog__mask{position:fixed;top:0;left:0;bottom:0;right:0;}...';appendToHead(css);

这种模式对CSS的开发维护很不友好,虽然我们不需要关注样式的细节,但还是每次要把重构发给我们的.css文件中的样式copy出来。新方案中,我们引入CSS module的方案。

2.技术选型


主流构建工具

构建工具的选择,主要对比了Webpack4、Rollupjs和Parcel,因为基础库的构建文件只有js,而且从构建体积来说,rollupjs是有绝对优势的,所以选择了rollupjs。

主流构建工具对比


CSS模块化

由于CSS in JS需要引入额外的依赖,在对比了CSS Module和CSS in JS后,我们选择CSS Module的方案。

 CSS模块化方案对比 


单元测试框架

单元测试框架我们选择了Jest,主要是因为开箱即用,不需要再引入断言库,生态也很好,较多用于React项目,而且组内的UI自动化测试系统是支持Jest的,这篇文章Migrating from Mocha to Jest中介绍了Airbnb的尝试。

 单元测试框架对比 


Lint方案

由于接入了CI系统进行lint自动化检查,为了减少“无效”的commit,我们选择了husky+lint-staged来进行本地代码提交前的lint。

Lint方案 


工作流和CI?

各种工作流中,首先需要在各自的开发分支进行开发测试,然后将代码合并到追踪生成环境的长期分支进行持续地发布部署,这意味着对这个长期分支要有完善的自动化测试能力,因为谁也不能保证merge的代码就一定不会有问题,目前新的方案引入了单元测试,对UI组件引入了基于puppeteer的截图测试,但一些功能缺乏在更多设备、更多平台上的自动化验证,因此我们认为在自动化测试方面的建设还不是非常完善,所以新方案接入了CI,但是对发布外链基础库music.js这种会直接影响到全量业务的并没有接入,还是使用ARS发布,除非紧急bug,其他的代码更改会在测试环境验证一段时间(一般2-3天)后才会发布外网。

我们的工程化实践

1.构建方案


新旧方案对比

首先可以看一下新旧构建方案的对比,在新方案中推广使用ES6,增加了对代码质量的控制:代码检查+单元测试,并接入了CI系统。

新旧方案对比 

打包方案

这是我们整体的打包方案,核心是一份源码开发维护,通过构建工具的差异化配置实现多种版本的构建。

打包方案 

开发流程

这是整体的开发流程,本地开发使用package.json管理项目依赖,规范代码格式,接入单元测试;提交之前git hook设置保证代码检查和测试通过后才能提交成功;使用QCI自动进行项目的构建、检查和测试,通过后将JSDOC文档推送到文档服务器,并发布npm包,外链js还是使用ars发布。

开发流程 

2.UI组件开发和文档

我们选择react-styleguide作为UI组件开发调试工具以及文档生成器,这是一个组件的MD文件示例:

### 组件式引入- 可以提前插入dom结构,如果浮层中有图片的话会先加载;- 属性中的 `visible` 控制组件是否可见。```jsximport Button from '../../basic/Button/Button'import QMDialog from './QMDialog';
class QMDialogExample extends React.Component { constructor(props) { super(props); this.state = {visible1: false} }

render() { const {visible1} = this.state; return (
<div> <Button onClick={() => { this.setState({ visible1: true }) }}>基本使用</Button> <Button onClick={() => { this.setState({ visible2: true }) }}>带头图的浮层</Button> <Button onClick={() => { this.setState({ visible3: true }) }}>传入一个react节点</Button>
<QMDialog visible={visible1} title="QQ音乐" message="这是一段描述" btn={'我知道了'} handleTap={index => { if(index === -1) { this.setState({ visible1: false }) } else { console.log('我知道了按钮被点击,index=', index) } }} /> </div>
) }}<QMDialogExample />```

react-styleguide会根据组件的源码和这个md文件生成文档和demo,开发调试阶段支持webpack配置HMR,非常方便。

 demo文档截图 


3.Jest单元测试

Jest可以设置全局的Setup,会在所有test执行之前运行,也可以设置全局Teardown,会在所有test执行完毕之后运行,比如这里就可以设置一些测试需要的Global对象、运行环境等等。describe可以将测试用例进行分组,beforeEach、afterEach、beforeAll、afterAll这些方法可以定义在测试用例之前或者之后运行的方法。


测试方案

根据上面介绍的打包方案和业务特点,基础库需要分别运行在node端和浏览器端,因此需要考虑到不同运行环境下的测试结果。


浏览器端
  • npm命令

    • jest --coverage --config ./config/jest/music.jest.config.js

    • 设置--coverage生成测试覆盖率。

  • 配置文件(music.jest.config.js):

    • 基于jsdom设置全局环境:jest-environment-jsdom-fourteen,提供浏览器端BOM对象。

    • 设置cookie操作权限的domain:testURL: "https://y.qq.com/m/demo.html",仅可以操作此域名下的cookie。

module.exports = {  clearMocks: true,  coverageDirectory: "jest-coverage/coverage-music-node",  preset: null,  rootDir: '../../',  testEnvironment: "jest-environment-jsdom-fourteen",  testMatch: [    "**/tests/music-node/**/*.test.[jt]s?(x)",  ],  testURL: "https://y.qq.com/m/demo.html",  transformIgnorePatterns: []};
Node端

node端和浏览器端的不同在于运行环境testEnvironment不同,jest提供jest-environment-node,我们为node端单独配置了music-node.jest.config.js。


UI组件

Jest支持对React App的测试,可以采用截图测试(Snapshot Testing)、模拟DOM操作(DOM Testing)等方法详见文档。在组件文档和demo这一章节中我们已经有了组件示例,并构建了文档页,可以直接接入团队的自动化测试系统,结合使用puppeteer进行截图对比。

下面是对QMDialog组件的测试用例,首先准备一张基准图片,然后写测试流程:打开页面——点击按钮触发组件——截图对比。screeshotDiff方法的实现参考了这篇KM文件通过puppeteer实现页面监控,图片diff核心算法由pixelmatch库实现。


const iPhone = devices['iPhone 6'];await page.emulate(iPhone);
await log("进入页面");await page.goto('http://[host]/reactui/index.html#/QMDialog', { waitUntil: 'load'});
await timeout(3000);let dom = await page.$('#QMPreload-container .rsg--preview-35 .button');
await dom.click();
await timeout(200)let diff = await screenshotDiff({ img: 'https://y.gtimg.cn/music/common/upload/t_cm3_photo_publish/1677163.png'});
if (diff > 10) { fail(); return;}
success();

这是一次测试运行结果,从左到右依次是:基准图、测试截图、diff结果图,screeshotDiff根据第三张图片返回差异点的占比,由于QMPreload组件的特点,加载进度受网络影响,设置阈值为10%,即只要差异率在10%以内就可以认为是正常的。

QMPreload测试结果 

和上面QMPreload不同,对QMDialog组件的判断则是需要差异值为0,如下面第三张图所示,没有差异点。

QMDialog测试结果 


mock

这是我们参照官网的文档接入的mock示例,这里需要注意__mock__的目录结构,详见文档。

.├── config├── src│   ├── music│   │   ├── utils│   │   │   ├── __mock__│   │   │      └── loadUrl.js│   │   └── loadUrl.js├── node_modules├── ...└── tests

loadURL方法用来动态加载js,使用jest.fn().mockImplementation对loadUrl进行mock,并mock了window.pgvMain和window.pgvSendClick。

export const loadUrl = jest.fn().mockImplementation((url, callback) => {    if (/ping.js/.test(url)) {        let pvCount = 0;        window.pgvMain = jest.fn().mockImplementation( (p1, p2) => {            expect(p1).toBe('');            expect(p2.virtualDomain).toBe('y.qq.com');            if (pvCount === 1) {                expect(p2.ADTAG).toBe('all');            }            pvCount++;        })        window.pgvSendClick = jest.fn().mockImplementation( (p) => {            expect(p.hottag).toEqual(expect.stringContaining('.android'));        });    }    callback();});
export default loadUrl;

因为使用了ES module的import,需要jest.mock对整个模块进行mock。对于mock的函数才能调用toHaveBeenCalledTimes的断言。

import tj from '../../src/music/tj';import loadUrl from '../../src/music/utils/loadUrl'
jest.mock('../../src/music/utils/loadUrl');
describe('【tj.js】点击上报', () => { test('tj.pv tj.sendClick', () => { expect(typeof window.pgvMain).toBe('undefined'); expect(loadUrl).toHaveBeenCalledTimes(0); tj.pv(); expect(loadUrl).toHaveBeenCalledTimes(1); expect(typeof window.pgvMain).toBe('function'); expect(window.pgvMain).toHaveBeenCalledTimes(1); tj.sendClick(); tj.sendClick('tjtag.click'); window.tj_param = { ADTAG: 'all' } tj.pv(); expect(loadUrl).toHaveBeenCalledTimes(1); expect(window.pgvSendClick).toHaveBeenCalledTimes(1); });})

测试覆盖率

这是某一次的测试报告,上面有每个模块详细的测试覆盖率。为了便于对各个模块灵活处理,我们将每个函数细分拆成一个文件,如下面的src/music/type目录下的各个文件。

测试覆盖率-1 

 

测试覆盖率-2 

测试覆盖率-3 

通过单元测试发现的代码bug

这些都是我们通过单元测试发现的之前一些函数的bug,仅举例一部分:

测试用例错误输出正确输出

M.type(undefined)

"nan"

"undefined"

M.isPlainObject(Object.creact({}))

false

true

Mozilla/5.0 (Linux; U; en-us; KFTT Build/IML74K) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.21 Safari/535.19 Silk-Accelerated=true<br />M.os.tablet

false

true

M.param({a: 1, b: {c: 1}})

"a=1&b=c%3D1"

"a=1&b%5Bc%5D=1"


4.一些Tips


声明pkg.module

声明pkg.module可以让构建工具利用到ES Moudle的很多特性来提高打包性能,比如利用Tree Shaking的机制减少文件体积,这篇文章package.json中的Module字段是干嘛的有详细介绍。

sideEffects

Tree Shaking可以在构建的时候去除冗余代码,减少打包体积,但这是一个非常危险的行为,在webpack4中,可以在package.json中明确声明该包/模块是否包含sideEffects(副作用),从而指导webpack4作出正确的行为。如果在package.json中设置了sideEffects: false,webpack4会将import {a} from 'moduleName'转换为import a from 'moduleName/a',从而自动修剪掉不必要的import,作用机制同babel-plugin-import。这个功能亲测是很有效的

对于rollupjs来说,有时候Tree Shaking并不有效,这是官网的一段解释,大意就是静态代码分析很难,为了安全rollupjs可能会无法应用Tree Shaking,这个时候建议最好还是明确import的PATH,这里可以结合适应上面的babel-plugin-import插件。

Tree-Shaking Doesn't Seem to Be Working 


插件

  • @babel/plugin-transform-runtime

这个插件可以避免每一个js文件分别引入胶水代码,而是整个构建文件引入一份胶水代码,减少代码体积。

  • eslint-friendly-formatter

对eslint的错误输出进行格式化,方便查看和定位问题。

  • babel-plugin-transform-react-remove-prop-types

由于运行时的性能原因,RN已经在production模式下移除了PropTypes,我们引入这个babel插件在生产模式中移除组件属性的类型校验相关的代码。

—noConflict

在将外链js用rollupjs构建成umd规范的时候,我们设置了--noConflict,可以解决全局变量M冲突的问题,类似于jQuery.noConflict()。

(function (global, factory) {    typeof exports === 'object' && typeof module !== 'undefined' ?        factory(exports) :        typeof define === 'function' && define.amd ?            define(['exports'], factory) :            (global = global || self, (function () {                var current = global.M;                var exports = global.M = {};                factory(exports);                exports.noConflict = function () {                    global.M = current;                    return exports;                };            }()));

参考文献

前端模块化详解(完整版)
我们是怎么进行前端工程化的
我对前端工程化的理解
浅谈 CSS 预处理器(一):为什么要使用预处理器?
CSS BEM 书写规范
CSS Modules使用详解
深入探讨前端组件化开发
CSS-in-JS,向Web组件化再迈一大步
jsx与模板dsl的优劣思考
从年会看声明式编程(Declarative Programming)
React & Web Components
The state of Web Components
用 husky 和 lint-staged 构建超溜的代码检查工作流
JEST
值得参考的css理论:OOCSS、SMACSS与BEM
浅谈 shadow dom 中的 template 和 slot
GitHub Flow & Git Flow 基于Git 的两种协作开发模式
git-flow的工作流程
Git工作流程
Introduction to GitLab Flow
软件测试的四个阶段,单元测试、集成测试、系统测试、验收测试
The Product Managers’ Guide to Continuous Delivery and DevOps
一文收录16张DevOps ”拍照神图”
DevOps漫谈之一:DevOps、CI、CD都是什么鬼?
Webhook到底是个啥?
Webpack中的sideEffects到底该怎么用?

因公众号不支持超链,想了解参考资料的腾讯可以前往:
https://cloud.tencent.com/developer/article/1500013

如果你对QQ音乐商业化Web团队感兴趣,想加入我们,请前往:
https://www.lagou.com/jobs/2944745.html

---------下方更多精彩----------

活动推荐

9月7日

腾讯滨海大厦

腾讯技术开放日来袭

聚焦5G国际标准和新兴媒体

权威领域专家为你带来最前沿的技术分享

揭开和解释许多关于5G的谜团和误解

点击阅读原文即可了解详情

-------END-------

关注云加社区,回复 3 加读者群

点在看,让更多人看到!

相关推荐

大模型的Agent Skill功能,在LLM HTTP底层交互流中是怎么承载的?

网络文章文本的 skill,昨天我实现了之后也就开始好奇起 Skills 的底层原理了——相比起各种花里胡哨的 agrnt 应用,我一直以来都很对大模型在纯粹的 HTTP 交互层面是如何交互的更感兴趣。Skill 的全部"魔法"都发生在 system prompt 的措辞和 SKILL.md 文件的编写质量上——本质上是一种 prompt engineering + 文件系统的组合。但是各种天花乱坠的 skills,是如何在底层的 HTTP 中,与大模型——这个只会嘴遁的工具交互的呢?

QcloudCommunity的博客 407

万字入门AI Infra:深入理解大模型中的数学与Infra优化

关注腾讯云开发者,一手技术干货提前解锁👇AI Infra科普、入门或者进阶 ~~ 不到 5 万字,带你融会贯通理解大模型的数学、算法、硬件到系统工程。任何问题欢迎沟通交流。可怕,我竟然有点认同这句话。当然大概率是因为姚顺宇太懂AI了,而我则是因为无知者无畏。不过仔细想想,大模型推理中的数学逻辑确实不复杂,很多原理只需要高中数学知识就能看懂。但是为什么像 vLLM 这种推理系统这么复杂呢?本文将拆解大模型中几个核心操作(RMSNorm、Softmax、Causal Mask、Sampling)背后的数学与

QcloudCommunity的博客 1167

平平无奇的源码,竟藏着Agent的核心秘密?

System Prompt 是剧本,Skill 是剧本的扩展包,Agent Loop 是演员按剧本行动。欢迎留言,我们将邀请作者针对性回复你的评论,我们将选取1则优质的评论,送出腾讯云定制文件袋套装。,让 Agent 能主动"巡逻",而不只是被动等待用户消息。OpenClaw 的 System Prompt 分层结构。的"行为底线",任何 Skill 或用户命令都无法覆盖它。精华 2:Skill 的"按需加载"避免 token 浪费。精华 1:System Prompt 是"分层乐高积木"

QcloudCommunity的博客 426

QQ音乐Harness Engineering实践

一个看似简单的需求,背后可能牵动服务调用链、配置、灰度、埋点、错误码、接口契约和历史兼容性。当 AI 开始快速生成大量代码,真正的瓶颈就不再是"写不出来",而是"看不完、想不清、管不住"。在对话式编码阶段,一种典型的工作模式逐渐流行:开发者把需求丢给 AI,扫一眼输出"看起来对"就直接采纳,不审 diff、不追问逻辑、不验证边界。以音乐商业化业务为例,一个需求可能从 TAPD 单开始,经过需求评审、技术方案、服务影响面分析、IDL 契约变更、业务代码实现、测试验证、CR、灰度上线,最后才进入稳定运行。

QcloudCommunity的博客 714

从Prompt、Context到Harness,工程的三次进化与终局之战

同时,强制要求散落各处的决策记录(Slack、邮件、文档)全部迁移至代码仓库,确保 Agent 的唯一信息来源是可信的、版本受控的仓库。更坑爹的是,研究表明,当上下文过长时,模型会出现"中间遗忘(Lost in the Middle)"现象:它对开头和结尾的内容记忆较好,对中间大段内容的关注度大幅下降。这篇文章,我想带你完整走一遍这三次进化的逻辑:它们分别解决了什么问题,它们之间是什么关系,它们的边界在哪里,以及:当三者融合,AI 工程师的终极形态究竟是什么?大语言模型的本质,就是这位金鱼助理。

QcloudCommunity的博客 718

让Skill自己训练自己:8阶段Loop、3层评测、5维AND门控,从此实现自进化

所以每一轮改动都是有据可查的,不是「感觉这里可以优化」,而是「Case 41 挂了,因为 agent 把离职问题路由到了邮箱分类,但正确答案在通讯录,所以我改 root_index.md 加个易混淆提示」。但让我最后决定把它记录下来的,不是这的个「吸收」过程,而是它真的在替我探索我不想费脑力触达的边界,这看起来更像是一个机器智能应该有的样子。不准跨层,每轮只改一层的东西。但我经过这 19 轮之后觉得,更准确的描述是互补——你在明处看着,AI 在暗处替你试错,你看不见的那一半就是 AI 能贡献的地方。

QcloudCommunity的博客 1127

只加两行代码,为什么要两天?一文深度理解业务系统的复杂性

关注腾讯云开发者,一手技术干货提前解锁👇业务系统复杂性一直是令开发者头痛的问题。复杂的不是增加一个需求需要耗费多少时间,而是在增加一个需求后带来的蝴蝶效应:其它功能会不会受到影响、要如何去找到这些影响,最终如何实现系统正常运行......功能之间隐秘增加的耦合、不可避免的代码腐化在导致业务复杂性增加。大家都在说的软件开发提效到底在提什么?程序员日常工作中应该如何提升开发效率?敏捷开发、瀑布流式开发孰是孰非?欢迎阅读。业务背景与目标我依然非常清晰地记得某个时候,我所在团队 Leader 曾讲过一段话:“我担

QcloudCommunity的博客 713

从零设计生产级 Multi-Agent Harness:架构、评估、记忆、成本与 MCP 工具接入全拆解

在 AI Agent 领域,Multi-Agent Harness 指的是把多个 Agent 的能力、工具、状态、通信、编排、监控统一收束在一个运行时之内的框架。第三,Agent 路由。一个输入框,一个大模型,几个工具,一段写得很漂亮的 System Prompt,再配上一个炫酷的前端,看起来就像一个能干活的“数字员工”。Multi-Agent 完全不一样——它有计划、有中间步骤、有工具调用、有 Agent 协作、有重试、有反思、有最终合成。增加 Budget、权限、重试、压缩、轨迹评估、审计、回归测试。

QcloudCommunity的博客 710

拆完Hermes源码,我发现Agent的“自我进化“根本不需要训练模型

两个月 5.2 万 Star,Hermes Agent 用一个"LLM 审判官"机制实现了 Agent 的自我进化——不改模型权重,只改"怎么用模型"的策略。OpenClaw(社区昵称"龙虾"🦞)是 2025-2026 年最火的开源 Agent 框架,30 万+ Star,插件市场 ClawHub 里有数千个插件,能接入 Telegram、WhatsApp、Slack、飞书、企业微信——几乎是一个"个人 AI 操作系统"。Skill 是英语写的,Memory 是英语写的,审查 Prompt 也是英语写的。

QcloudCommunity的博客 909

AI Infra 其实没有多少新东西

传统 Infra 追求横向扩展,而 AI Infra 呈现 “AI 大型机”特性,是因为传统后台服务的可以容忍毫秒级延迟,但 AI 集群不行,GPU 的算力是 CPU 的数百倍,微秒级的延时等待也会造成很大的算力损耗,需要硬件的高度集成。在可预见的1-3年的未来,这样的专用硬件+网络的集中式架构很难发生比较大的改变。与之相似,模型推理的批处理就是将多个输入样本打包(batch),将原本串行的 N 次轻量的推理计算,合并为 1 次重量的计算,实现单位时间内处理更多的请求,提高了 GPU 利用率。

QcloudCommunity的博客 825

程序员越早想通这些越好

永远没有正确架构,你永远无法偿还完所有的技术债务,如同你永远不会设计出完美的界面,请避开完美主义的陷阱,永远没有完美的代码,能够满足当前需求,又为未来不管是自己还是他人留有扩展空间,就很好了。我们努力的方向就是将现实世界映射到有限的数据结构中,这就需要用户自定义类型,如果你的代码中包含类似 trader portfolio 这样的概念,你就可以使用他们的名称来建模,而不是使用基本数据类型复杂嵌套表述你的目的。从另一个角度看,代码里的注释都是补丁,是碎片化的,局部的注释很难推导模块整体的顺序及耦合关系。

QcloudCommunity的博客 465

RAG已死?不,是Grep回归了!

这样一来可以直接复用已有的计算缓存,只为新增的增量部分付全价,前面累积的大头按约 1/10 的缓存价计费。所以实际的工具组合方式是灵活的:Grep(默认模式)→ Read 是最常见的路径,但 Grep(content 模式)可以独立使用,LLM 也可以直接调 Read(如果已经知道文件路径),或者一次同时发起多个 Grep 并行搜索。它从零开始构建自己的对话历史,不继承主对话的消息,这意味着它搜索过程中产生的大量 grep 结果、代码片段都留在自己的 context 里,主对话只收到一段总结性的文本结论。

QcloudCommunity的博客 576

深入浅出Harness Engineerring之核心模式与理念

在智能体生命周期的关键节点(如会话开始、工具调用后)自动触发预设动作(如代码格式化),由系统确保关键流程被执行,不依赖可能被模型遗忘的指令。简单讲就是利用向量存储这个技术,来实现模糊检索,原理是:即使字面不同,但语义相近的文本,其向量在数学空间中的位置也很接近。根据命令类型、参数和影响,自动评估其风险等级(安全、有风险、危险),并采取自动执行、请求确认或直接拦截等不同策略。关键特性是可隔离、可重建、可扩展。不是一次判断就定终身,允许你改变、允许情况复杂,通过不断观察、思考、调整,越来越懂真实的你。

QcloudCommunity的博客 624

Harness Engineering实践心得:如何高效驾驭AI?

但如果这些经验被拆成一条条 Rule、一个个 Skill、一项项 Script 检查——"XAML 不能有中文"、"编译要用这个路径的 MSBuild"、"SVN 命令必须带认证参数"——AI 就能在秒级时间里"吸收"十年的踩坑经验。然后可以看到下方的图,我的PM会很顺利的调度其他agent分别负责不同的事项,遇到问题的时候也能准确的找到合适的agent。MCP 本质上是在扩展 AI 的"感知范围"——Rule 管"该怎么做",Skill 管"怎么操作",MCP 管"能看到什么、能碰到什么"。

QcloudCommunity的博客 842

从第一性原理思考 Agentic Engineering

需要注意的是,这一结论是有条件的:如果 AI 缺乏领域知识,过早介入反而可能引入 1.2 节所述的"似是而非"损耗——因此上下文供给(实践 1)是 AI 全链条参与的前提,而非结果。通过一个专门的自迭代 Skill 实现——当 AI 在协作过程中识别到自己被纠正(用户否定了 AI 的输出并给出了正确方向),它自动评估这个错误的根因,搜索现有的 Rules 和 Skills 中是否已有针对该类问题的处理规则。事实上,越是复杂的项目,工程师的认知负担越重(公理 3),AI 分担认知负担的潜在边际价值也越大。

QcloudCommunity的博客 757

万字干货!Harness Engineering如何工程化落地?

关注腾讯云开发者,一手技术干货提前解锁👇Harness Engineering 的概念已经火了有一阵了,全网很多文章基本都是在讲理念,讲为什么今天做 AI 开发,不能只靠一段提示词,也不能把模型当一个“更聪明的代码补全”。但与此同时,一个问题也在持续地发酵:这个问题特别重要。因为 Harness 这个词听上去很大,很像一个抽象的方法论,但它如果不能落到工程、目录、文档、脚本、工作流里,那它最后就只是一句漂亮的话。事实上,不同项目落地出来的 Harness,表面上可能完全不一样。有的项目是 CI 很强,有的

QcloudCommunity的博客 1169

全方位对比:Hermes VS OpenClaw

多账号矩阵运营 如果你的业务涉及同时管理多个社交媒体账号、多个平台的内容分发、多渠道的客户互动——OpenClaw是目前市场上的"无可替代的底座"。一个有专业背景的个人,现在可以快速构建一个垂直领域的AI助手:比如"财务分析Hermes"、"法律咨询Hermes"、"技术架构Hermes",直接通过微信提供服务,积累自己的专业IP。跨平台生态融合 如果你的工作流涉及飞书、Slack、Discord等多个平台的深度集成,需要这些平台之间的数据流转和联动——OpenClaw的生态支持是强大的。

QcloudCommunity的博客 1217

一文讲透:Harness Engineering即控制论!

而研发模式的升级我认为将会是一个飞跃的过程,之所以现在我们仍然需要需要参与编码,一个非常大的原因就是缺乏传感器和基建,仍然需要人来串流程,比如需要人来验证UI是否正确、需要人来告诉AI代码不符合代码规范,因此在有些场景我们仍然感觉 AI 完成的很吃力,但这不是 AI 能力不行,而是我们并没有为 AI 提供友好的环境……控制论中的突变理论介绍了矫枉过正和飞跃现象之间的矛盾,如果质变中经历的中间过渡态是不稳定的,那么他就是一个飞跃的过程,如果中间过渡态是稳定的,那么他就是一个渐变的过程。但是这些阶段要解决的。

QcloudCommunity的博客 1204

神级开源插件!一句话让QClaw变3D小镇,Agent工作可视化,赛博云监工

这个架构是翻了几次车才想通的—原有AI的实现机制是用 setTimeout 猜动画时长导致了幽灵 NPC、工位撞车、越改越崩,后面我停下来换了思路,推翻原有的架构,按照“意图+回传”的架构进行重构,之前的问题一次性全部解决。15 天,我和 AI 吵了 185 次架, 4.5 万行代码,开发了一款 QClaw / OpenClaw 的UGC游戏插件,这可能是人类跟 Agents 交互的新范式,插件已完全开源,有需要的欢迎自取~我天天用,用着用着我就在思考,Agent 产品现在的交互体验,基本都一个样。

QcloudCommunity的博客 1196

一文搞懂Hermes:新顶流Agent如何从经验中自我进化

Voyager 是一个能在 Minecraft 中自主探索、学习技能、无限积累能力的 AI Agent,它提出了 "Skill Library"(技能库)的概念——Agent 将成功的行为序列编码为可复用的技能函数,存储在一个持续增长的代码库中。如果 Skill 的名称和描述不够精准,或者用户的任务描述与 Skill 的触发条件有语义差距,Agent 可能会错过相关的 Skill。如果进程在写入过程中崩溃,目标文件要么是旧内容(还没被替换),要么是新内容(替换已完成),绝不会出现写了一半的损坏文件。

QcloudCommunity的博客 2635

自掏腰包一万元,拥抱AI这一年,我的工具、实践和思考

其实也很好理解,过往的每个领域(比如前端、后端、设计、测试等等)都主要是这个领域的从业者在为它添砖加瓦,但是大模型时代的 AI 应用领域却几乎是所有使用 AI 的人类一起在迭代,无分领域,无分文理,无分老少。因为每个时期的 AI 都有着不同的特点,很多时候当我费尽心思去钻研,自以为有点弄明白 AI 应该怎么玩的时候,基座模型和行业范式常常就会迎来颠覆式的变革,很多过往的经验都会倾覆,然后只能从头再来。过去一年多,被热议的 AI 范式大概是沿着四个阶段递进演化,每一个阶段其实都在解决前一个阶段的核心问题。

QcloudCommunity的博客 627

办公Agent的CI/CD时刻到来了

核心观点是:企业给员工配了一堆 AI 工具,试点跑了几百个,但真正拖慢 AI 转型速度的瓶颈不在模型能力,在"最后一公里":AI 输出和实际工作流之间的那道鸿沟。代码写完,打个 tar 包,开 FTP 传到服务器上,SSH 进去解压,改配置文件,重启服务,刷一下页面祈祷别 500。你复制,切窗口,粘贴到腾讯文档里,发现 Markdown 格式全乱了,手动调标题层级,代码块重新标记语言类型,表格散架了重排一遍。模型是越来越聪明了,但"生成"到"落地"之间那段路,还是你自己在走,一步一步,手动搬运。

QcloudCommunity的博客 608

4亿token买来5个教训:让6个AI Agent连写4天代码发生了什么?

Watchdog 检测到崩溃 → 启动诊断 Agent 分析原因 → 诊断 Agent 跑满 20 轮没搞定 → Watchdog 判定“已修复”,重启系统 → 重启的系统因为诊断 Agent 留下的环境变量被继承,连续 3 次拒绝启动 → 触发连续失败保护退出 → Watchdog 第 3 次巡检,发现进程和记录文件都没了,尝试手动重启,自己也停了。启动前我翻了论文、做了架构设计、写了监控+修复异常的脚本。这个发现让我的情绪从沮丧变成了兴奋——不是那种"终于修好了"的释然,是"我找到根因了"的快感。

QcloudCommunity的博客 584

赛博斗蛐蛐:9大模型决战三国志,天命在谁?

我猜是这样的,当Gemini面临决策的时候,一系列关键词比如 守城,劣势,粮草,三国等等,在语义层面的关联性中LLM发现了竖壁清野这个关键词概率非常高,所以以此为策略,然后再根据这个策略完美执行操作。MiMo 的外交是三方中最精明的。Kimi作为国产模型,也同样颠覆了我对他的看法,最后的比赛他甚至一度是优势者,存在很多夺冠的可能性,要知道他的对手可是claude和gemini,而他经常出兵慢,回过头来想想或许正是他的策略,毕竟中后期他的优势都来自于稳健的发展和较少的战斗频率(和claude比).

QcloudCommunity的博客 874

Claude Code是怎么知道你在骂他的?这 12 条发现值得关注

确实存在——5,500+ 行,集成了消息队列、工具装配、MCP 编排、会话状态、权限、分析、文件持久化和优雅关闭,基本上一个文件干了所有事。对抗虚假声明——"不要在输出显示失败时声称所有测试通过,不要压缩或简化失败的检查来制造绿色结果"。完整的 KAIROS 是一个持续驻留的守护进程,能感知你在不在电脑前、主动寻找有用的工作做,更像是住在终端里的全职同事。源码里藏着一个叫 "Buddy" 的电子宠物系统,完整度令人惊讶——这不是一个 TODO,而是一个从数值系统到 ASCII 动画都实现了的完整功能。

QcloudCommunity的博客 701

逆向深扒Claude Code源码,我发现了什么!?

Anthropic 内部用户获得更严格的质量控制 prompt、更好的验证机制(独立 Verification Agent)、以及针对 model 弱点的专门对策(如 Capybara v8 的 29-30% 错误声明率对策)。这不是代码混乱的标志——它是整个 Agent Loop 的心脏,所有的流式处理、工具调度、上下文压缩、子代理管理都在这个文件中协调。这种"胖核心"设计是刻意的:保持核心循环的原子性,避免分散到多个模块导致的协调复杂度。核心价值:Fork 代理的工具输出不会回到主代理的上下文窗口。

QcloudCommunity的博客 892

一文讲透如何构建Harness——六大组件全解析

本文将从"裸模型的四个硬伤"出发,逐一拆解 Harness 的六大组件——文件系统、Bash + 沙箱、记忆(AGENTS.md)、Web Search + MCP、上下文工程、编排 + Hooks——带你看清 AI Agent 真正的竞争壁垒,不在模型层,而在 Harness 层。如果你用过 Agent 做复杂任务,你一定有过这样的体验:一开始 Agent 的表现很好,但随着对话变长,它开始"变笨"——遗忘之前的约定,重复之前犯过的错误,甚至对同一个问题给出前后矛盾的回答。对于简单的问答场景,这无所谓。

QcloudCommunity的博客 761

Harness Engineering 来了,SDD 还有意义吗?

他们的解法是把「品味」编码进规则,然后用一个定期运行的「doc-gardening」Agent 自动扫描漂移、发起修复 PR——「技术债务就像一笔高息贷款,不断地以小额贷款的方式偿还,总比让债务不断累积要好」。代码仓库本地的、已版本化的工件,才是 Agent 所能看到的全部。在 Vibe Coding 模式下(即不做规划、随意向 AI 提需求的开发方式),大量设计决策、跨服务约定、「为什么不选另一个方案」的讨论,全部停留在一次性的 AI 对话里——下一个人,或者下一次 AI 会话,完全无从获取。

QcloudCommunity的博客 419

零废话!一文讲透从0构建AI Agent

每个工具都要写一套"定义→解析→执行"的胶水代码。上下文管理就是在"记住足够多"和"别超标"之间找平衡,常见策略包括滑动窗口、摘要压缩、RAG 检索增强。上下文窗口是 Agent 的"工作记忆",但它有硬性限制。LLM 负责"动脑"决策,代码负责"动手"执行。这是从"聊天机器人"进化到"Agent"的关键一步。每次调 API 都把完整对话历史发过去,LLM 本身是无状态的,"记忆"完全靠客户端的数组模拟。这样 Agent 一开始就知道自己"在哪里"、"手头有什么",不需要先花一轮工具调用去探索环境。

QcloudCommunity的博客 711

距离AI把小编我裁掉还有72小时

这是一个基于 OpenClaw 开源生态打造的本地化 AI Agent 助手,已于上周开启全量公测,无需复杂配置,三步即可上手,自带安全防护,支持微信直连绑定,内置多款优质国产大模型,现阶段也是完全免费使用,基本上前面提到过的痛点都可以靠它解决。对了,这里特别强调一点,上面的截图让产品同事汗流浃背,再三找我确认 QClaw 思考过程中优美的“草”字说法,不会是默认出现的吧?AI 好,AI 得学,但更重要的是,在你的工作和生活里,哪些东西是能结构化地被 AI 所接入的,不要死守着那些脏活累活不放。

QcloudCommunity的博客 673
上一篇: 开发小游戏,云开发真的是银弹吗?
下一篇: yarn 在快手的应用实践与技术演进之路
腾讯云开发者
腾讯云开发者 企业官方账号 企业官方账号
博客等级 码龄10年 6291粉丝 1962原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值