探索React、Vue等现代框架出现之前,前端开发者如何实现代码复用和组织,以及为什么我们需要新的解决方案
前言:一个时代的挑战
在React、Vue等现代框架诞生之前,前端开发是一个充满挑战的领域。想象一下:你需要管理大量相互依赖的脚本文件,手动处理DOM更新,在全局作用域中挣扎避免命名冲突,那时还有专门生成命名的脚本,还要保证浏览器兼容性。这就像在没有蓝图的情况下建造一座摩天大楼------容易出错且难以维护。
让我通过一个真实的故事开始:几年前,我参与维护一个jQuery时代遗留的大型电商网站(深圳棒客科技)。代码库中有超过100个JavaScript文件,全局变量冲突是家常便饭,一个按钮的点击事件可能在5个不同的文件中被处理。每次修改都像在走钢丝,因为我们永远不知道会影响到哪个角落的功能。这种痛苦经历让我深刻理解了现代前端框架的价值。后来我花三个月重构了公司的站点,那会是Vue初代。
1 原生JavaScript的模块化尝试
在ES6模块化标准之前,JavaScript本身没有模块系统。开发者不得不创造性地解决这个问题:
IIFE模式(Immediately Invoked Function Expression)
javascript
// 模块A
var ModuleA = (function() {
var privateVar = 'I am private';
function privateMethod() {
return privateVar;
}
return {
publicMethod: function() {
return privateMethod();
}
};
})();
// 模块B
var ModuleB = (function(modA) {
// 依赖注入
return {
useModuleA: function() {
return modA.publicMethod();
}
};
})(ModuleA);
优点 :创建了私有作用域,避免全局污染
缺点:模块间依赖关系不够清晰,需要手动管理加载顺序
命名空间模式
javascript
// 创建一个全局命名空间
var MyApp = MyApp || {};
// 在命名空间下添加模块
MyApp.Utils = {
formatDate: function(date) {
return date.toISOString();
}
};
MyApp.Components = {
Button: function(text) {
this.text = text;
}
};
基于原型的模块化
javascript
// 类似类的定义
function User(name, email) {
this.name = name;
this.email = email;
}
// 原型方法 - 所有实例共享
User.prototype.sendEmail = function(message) {
// 发送邮件逻辑
console.log(`Sending to ${this.email}: ${message}`);
};
// 使用
var user1 = new User('Alice', 'alice@example.com');
var user2 = new User('Bob', 'bob@example.com');
1.2 CommonJS和AMD规范
随着Node.js的出现,CommonJS成为服务器端模块标准,而AMD(Asynchronous Module Definition)则是为浏览器设计的:
javascript
// CommonJS风格(Node.js/通过Browserify打包后)
var $ = require('jquery');
var utils = require('./utils');
module.exports = function() {
$('body').append('<div>Hello</div>');
};
// AMD风格(RequireJS)
define(['jquery', './utils'], function($, utils) {
return {
init: function() {
$('#app').html(utils.format('<div>{0}</div>', 'Hello'));
}
};
});
// RequireJS配置
requirejs.config({
paths: {
'jquery': 'https://code.jquery.com/jquery-3.6.0.min'
}
});
requirejs(['app'], function(app) {
app.init();
});
个人体会:我曾经在一个大型项目中使用RequireJS,虽然解决了依赖管理问题,但配置文件的复杂度让人头疼,而且需要额外学习API。
1.3 构建工具的革命
Grunt和Gulp的出现,知道今天,如果你个人编写小型的库,这两个工具依然可靠高效。
javascript
// Gruntfile.js示例
module.exports = function(grunt) {
grunt.initConfig({
concat: {
js: {
src: ['src/**/*.js'],
dest: 'dist/app.js'
}
},
uglify: {
js: {
src: 'dist/app.js',
dest: 'dist/app.min.js'
}
}
});
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.registerTask('default', ['concat', 'uglify']);
};
Browserify和Webpack的兴起
Browserify让开发者可以在浏览器中使用CommonJS语法,Webpack则更进一步,支持多种模块格式和资源打包。
HTML/CSS的复用方案
2.1 HTML模板技术
服务端模板
php
<!-- PHP include -->
<?php include('header.php'); ?>
<div class="content">
<?php echo $content; ?>
</div>
<?php include('footer.php'); ?>
<!-- JSP include -->
<%@ include file="header.jsp" %>
<jsp:include page="sidebar.jsp" />
客户端模板引擎
javascript
// Handlebars模板
<script id="user-template" type="text/x-handlebars-template">
<div class="user-card">
<h2>{{name}}</h2>
<p>{{email}}</p>
<button class="btn-{{type}}">{{buttonText}}</button>
</div>
</script>
<script>
// 编译和使用模板
var source = document.getElementById('user-template').innerHTML;
var template = Handlebars.compile(source);
var html = template({
name: 'Alice',
email: 'alice@example.com',
type: 'primary',
buttonText: 'Follow'
});
document.getElementById('container').innerHTML = html;
</script>
基于字符串拼接的组件
javascript
// 创建可复用的UI组件
function createButton(config) {
var className = 'btn ' + (config.type || 'default');
var disabled = config.disabled ? 'disabled' : '';
return '<button class="' + className + '" ' + disabled + '>' +
config.text +
'</button>';
}
// 使用
var buttonHtml = createButton({
text: 'Click me',
type: 'primary'
});
2.2 CSS的复用和组织
OOCSS(面向对象的CSS)
css
/* 结构类 */
.container { width: 100%; max-width: 1200px; margin: 0 auto; }
.row { display: flex; }
/* 皮肤类 */
.btn { padding: 10px 20px; border: none; cursor: pointer; }
.btn-primary { background: blue; color: white; }
.btn-secondary { background: gray; color: white; }
/* 使用 - 组合类名 */
<button class="btn btn-primary">Submit</button>
<button class="btn btn-secondary">Cancel</button>
BEM(Block Element Modifier)方法论
css
/* Block */
.card { }
/* Element */
.card__title { }
.card__content { }
.card__footer { }
/* Modifier */
.card--featured { }
.card--disabled { }
/* HTML结构 */
<div class="card card--featured">
<h2 class="card__title">Featured Card</h2>
<div class="card__content">...</div>
<div class="card__footer">...</div>
</div>
CSS预处理器
css
// SCSS中的mixin和继承
@mixin button-style($bg-color) {
padding: 10px 20px;
background: $bg-color;
color: white;
border: none;
border-radius: 4px;
&:hover {
background: darken($bg-color, 10%);
}
}
.btn-primary {
@include button-style(blue);
}
.btn-secondary {
@include button-style(gray);
}
// 继承
.error-message {
border: 1px solid red;
color: red;
padding: 10px;
}
.validation-error {
@extend .error-message;
font-weight: bold;
}
3.1 jQuery插件系统----组件化的早期尝试
这是那个时代经尝考到的的面试题之一
javascript
// 定义jQuery插件
(function($) {
// 私有方法
function privateMethod() {
console.log('This is private');
}
// 插件定义
$.fn.myCarousel = function(options) {
// 合并配置
var settings = $.extend({
speed: 300,
autoplay: false
}, options);
return this.each(function() {
var $this = $(this);
// 初始化逻辑
$this.addClass('carousel-container');
// 公开方法
$this.data('carousel', {
next: function() {
// 切换到下一张
},
prev: function() {
// 切换到上一张
}
});
});
};
})(jQuery);
// 使用插件
$('#myCarousel').myCarousel({
speed: 500,
autoplay: true
});
// 调用插件方法
$('#myCarousel').data('carousel').next();
看了以上早期的'组件化'开发和思维方式后,再来对比为什么需要React这样的框架,显而易见。感谢时代巨人们付出的智慧。