组件模块化最佳实践
来源:互联网 发布:web前端开发 薪资知乎 编辑:程序博客网 时间:2024/06/06 00:55
4.1 什么是模块化?(What)
我们先来看这样一个问题:
假设有一个module1.js文件里面提供了一个方法:
//module1.jsfunction foo(){ console.log("bar1");}
但是由于实现的需要,我们必须引入另外一个文件module2.js,其中恰好也包含了一个函数foo的实现
//module2.jsfunction foo(){ console.log("bar2");}
调用的时候,我们会先引入2个js文件然后直接执行foo方法:
//调用代码foo();
那么当我们同时引用这两个文件的时候,foo函数的输出就取决于两个文件的加载顺序,这样就出现了命名冲突的情况,为了解决这样的问题,我们可能想到了通过封装成对象的方式来对每个模块加上命名空间:
//module1.jsvar module1={ foo:function(){ console.log("bar1"); } }//module2.jsvar module2={ foo=function(){ console.log("bar2"); }}
这样我们调用的时候就可以分别使用module1和module2作为命名空间来隔离不同的foo函数
//调用代码module1.foo();module2.foo();
通过上述方式,我们定义了命名空间,对相同命名的函数起到了隔离作用。这就是模块化思想中的第一部分:冲突隔离
再来看另外一个问题:
假设我们要做一个网站,有不同的页面,每个页面可能需要不同的js文件,这些js之间存在依赖关系,比如bootstrap或者某些jquery插件可能是依赖jquery的,那么传统的做法是:
<script src="path/to/jquery/jquery.js"><script src="path/to/bootstrap/bootstrap.js">
这样做很重要的一点是,我们必须按照正确的顺序来引入javascript,如果javascript文件比较少,这样做无可厚非。然而当一个系统规模越来越大,更极端的情况是当我们希望通过模块化的方式来做一个Web App的时候,所有的javascript文件都会被放到一个页面上,或者按照合理的顺序被压缩在一个文件中,这些javascript文件中的一个可能是控制某一个页面的,但是他的执行又依赖于其他一些模块或者第三方的库,必须等待这些模块或库提前被加载和初始化,这个时候问题会变得很复杂,我们需要手动的调整javascript文件的位置,以防止载入的时机不正确引起的问题。当文件数量比较多的时候,人肉的做法显然是不科学的。
我们如何去模块化地区分这一个个javascript文件,专业地说,我们如何解决这样的耦合问题?首先可以排除这样通过手动排列javascript的方式,因为这样的做法毫无模块化可言。再者如果考虑到团队合作,这样的程序迭代会让我们变得疯狂。
模块之间需要按顺序加载,这就是模块化思想的第二部分:依赖管理
在此,我们先提出一个概念,至于如何实现依赖管理,我们将在下文中进行叙述。
第三个问题,假设我们解决了命名冲突,解决了依赖管理。当我们有很多不同模块的时候,我们如何实现根据需要来进行加载。什么叫根据需要进行加载?就是对于某一个模块,加载的时候首先要加载一些依赖模块,而不加载一些冗余的模块。用我们熟悉的java来举个例子:
当我们需要使用到List类的时候,我们可能会在文件头上加上import语句
import java.util.List;
当然如果你是C#程序员你可能会这样写 :
using System.Collections.Generic;
无论你用什么语言这样做的目的是加载当前需要使用的包/命名空间,当你只需要List类的时候,你就不需要引入其他无关的包,如此一来,类的依赖显而易见。那么,javascript能不能像这些语言一样,也做到根据需要进行模块的加载呢?答案是,能,怎么做?且听下文。
这里就引入了模块化思想的第三部分:按需加载
小结
在这一节中,我们通过3个例子,引入了模块化的3个思想:冲突隔离、依赖管理和按需加载。如果通过本小节的阅读,若还是无法理解这3个思想,没关系,接下去还会继续围绕这3个思想进行深入探讨,各位姑且对这3个思想留下个印象,这也是本文后续章节的写作线索。
4.2 为什么要使用模块化?(Why)
假设一个不断扩展的系统,如何保证js的命名不冲突?
现在的项目使用越来越多的第三方js库,这些库之间又有复杂的依赖关系,如何友好的处理不同的库之间的依赖关系?
传统的Web Page功能不断丰富,开始转化为Web App,如何构建一个模块分明易维护的Web App?
过程式的写法不能适应不断扩展的网站功能, 因此出现了面向对象的Javascript,如何利用面向对象的特性, 构建高可维护性的前端代码?
为了解决上述的这些问题,答案就是,使用模块化。
4.3 如何使用模块化?(How)
第一小节中我们讨论了模块化的3个思想,留下了一个悬念,我们通过什么样的工具,可以做到依赖管理和按需加载呢?
其实我们想要的,是否是这样的一个方式:
var module1=require("/path/to/module1/module1.js");module1.foo();
在Javascript模块化发展的过程中,慢慢的出现一些强大的模块化管理工具,就实现了这样的效果。关于前端模块化的发展历史,推荐阅读前端模块化开发那点历史,通过阅读这篇文章,我们可以对一下内容有一个基本的了解
- CommonJS模块化社区
- Modules/1.0和NodeJS的关系
- Modules/2.0和SeaJS的关系
- AMD/RequireJS的起源
- AMD与CMD的关系与区别
暂时不谈服务端NodeJS,目前前端模块化主要有两个流派:以RequireJS为代表的AMD流派和以SeaJS为代表的CMD流派。
CMD和AMD
CMD的意思就是通用模块定义(Common Module Definition),是SeaJS推广过程中对模块定义的规范化产出
AMD的意思就是异步模块定义(Asychronous Module Definition),是requreJS推广规程中对模块定义的规范化产出
想要更多地了解CMD和AMD,可以移步:
- CMD标准
- AMD标准
下面我们分别对这两个代表性的前端模块加载框架进行简单介绍:
4.4 Require.js简介
RequireJS is a JavaScript file and module loader. It is optimized for in-browser use, but it can be used in other JavaScript environments, like Rhino and Node. Using a modular script loader like RequireJS will improve the speed and quality of your code.
这是一段摘自RequireJS官方的简介
RequireJS是一个JavaScript文件与模块加载工具,它优化了浏览器中JavaScript的使用方式,但是它也可以被用在其它环境中比如Rhino和Node,通过使用像RequireJS这样的模块加载工具,可以显著提高你的网站速度和代码质量。
在此文中,我们只谈RequireJS的主要功能,即它在浏览器中的使用方式。
RequireJS使用的方式很简单,我们只需要关注3个方法:requirejs.config
方法define
方法和require
方法。
requirejs.config方法
require.config
做了3件事情:定义目录结构、定义依赖关系、定义非模块化JS的引用方式
requirejs.config({ baseUrl: 'js/common',//root目录,相对于当前文件的目录 waitSeconds: 60, //加载超时时间 paths: //文件目录,相对baseUrl/root的目录 { model: '../model', view: '../view', template: '../template', moduledir::'../modules' }, shim: //非模块化js引用定义 { 'jquery': { exports: '$' }, 'backbone': { deps: ['underscore', 'jquery', 'json2'],//定义依赖 exports: 'Backbone' }, 'underscore': { exports: '_' }, 'json2': { exports: 'JSON' }, 'jquery-ui': { deps: ['jquery'], exports: '$' }, 'colorpicker': { deps: ['jquery'] }, 'gmap3.min': { deps: ['jquery'] } }});
这里重点解释一下非模块化JS代码
非模块化代码指的是没有根据模块化的要求进行封装的js代码, 其调用接口一般暴直接露在window对象中。
值得注意的是这里的所有js都没有.js
后缀,道理很简单,程序员就是喜欢少敲几下键盘
上面我们看到的requirejs.config
中的exports就是显式地告知当前文件暴露在window中的接口,这个 接口/引用对象 将被传入require
或者define
方法所在的函数作用域内。
define方法
define
的作用是定义一个模块,最简单的用法是:
//module2.jsdefine( //可选参数,定义模块的名字 "module2", //依赖列表 ['moduledir/module1','jquery'], //加载完依赖后的回调函数 function(module1){ var module2={ //定义一个模块... }; //返回这个模块(重要) return module2; });
在上面的代码中,我们定义了一个模块module2,第二个参数指明了这个模块依赖于module1和jquery(不分先后顺序),所以在加载module2的时候,会首先递归地去把所有依赖的都加载完,等所有的模块都加载完后,会把这些依赖模块对应的引用传入第三个参数既回调方法中,然后在这个回调方法的作用域里面,我们就可以使用jquery和module1提供的功能了。
原理并不难,通过一个define方法,传入依赖列表,define方法通过异步加载模块的依赖,最后把每个依赖模块的引用传入到回调方法中。
有两点是值得我们注意的:
- 为什么是一个回调方法?因为这个过程是异步的,我们不希望这个过程中浏览器被阻塞
- define方法必须有一个返回值,来告诉外部,当前模块的对外接口是什么,这一点是经常被忽视的
require方法
让我们想象一下java程序、C++、Objective-C或者C#程序,他们执行的时候,都有一个Main函数,既程序的入口函数。使用requirejs的时候,入口函数就是require
方法。
//main.jsrequire( //依赖列表 ['app','jquery'], //加载完依赖后的回调函数 function(app) app.init(); });
对比一下require
方法和define
方法,可以发现区别是require
方法不需要返回值,很容易理解,require
方法只需要调用而不需要为其他模块提供接口
4.5 Sea.js简介
SeaJS提供了7个基本的API:
seajs.config
用来对 Sea.js 进行配置
seajs.config({ // 设置路径,方便跨目录调用 paths: { 'arale': 'https://a.alipayobjects.com/arale', 'jquery': 'https://a.alipayobjects.com/jquery' }, // 设置别名,方便调用 alias: { 'class': 'arale/class/1.0.0/class', 'jquery': 'jquery/jquery/1.10.1/jquery' }});
seajs.use
用来在页面中加载一个或多个模块。
// 加载一个模块seajs.use('./a');// 加载一个模块,在加载完成时,执行回调seajs.use('./a', function(a) { a.doSomething();});// 加载多个模块,在加载完成时,执行回调seajs.use(['./a', './b'], function(a, b) { a.doSomething(); b.doSomething();});
define
用来定义模块,遵循统一的写法:
define(function(require, exports, module) { // 模块代码});
require
, exports
和module
三个参数可酌情省略
require
require
用来获取指定模块的接口。
define(function(require) { // 获取模块 a 的接口 var a = require('./a'); // 调用模块 a 的方法 a.doSomething();});
require.async
用来在模块内部异步加载一个或多个模块。
define(function(require) { // 异步加载一个模块,在加载完成时,执行回调 require.async('./b', function(b) { b.doSomething(); }); // 异步加载多个模块,在加载完成时,执行回调 require.async(['./c', './d'], function(c, d) { c.doSomething(); d.doSomething(); });});
exports
用来在模块内部对外提供接口。
define(function(require, exports) { // 对外提供 foo 属性 exports.foo = 'bar'; // 对外提供 doSomething 方法 exports.doSomething = function() {};});
module.exports
与exports
类似,用来在模块内部对外提供接口。
define(function(require, exports, module) { // 对外提供接口 module.exports = { name: 'a', doSomething: function() {}; };});
4.6 RequireJS和SeaJS的对比与选择
通过上文的描述,如果你对NodeJS有所了解,那么你会更倾向于SeaJS的风格,SeaJS提供的接口毕竟继承了很多Module1.x的风格。
两者都是非常优秀的模块加载框架,伴随的争论也从来没有停止,我们在此便不过多的争论AMD和CMD的优劣,对于同样优秀的两个框架,我们不能持着先入为主的观念,实际上,在RequireJS2.x中,也加入了对CMD的支持,而SeaJS中也有异步的方式,两者正在互相取长补短,争论没有让两者越离越远,而是越来越接近。
所谓弱水三千只取一瓢饮,正因为他们越来越接近,所以我们无需担心会不小心做出错误的选择,那我们不妨先好好了解和使用其中一个。
结合恒天目前的项目经验,我们选择了RequireJS。
4.7 RequireJS最佳实践
关于RequireJS的基本用法,已经在前面进行了简单的描述,然而这些基本用法并不能帮我们解决很多问题,也并不能完全展现出RequireJS给我们带来的便捷与优点。下方罗列出的,可能是我们在项目中使用RequireJS后经常需要考虑的:
- 异步加载非js文件,如html模板
- 单页应用中使用RequireJS
- 多页应用中使用RequireJS
- 代码合并压缩
接下来的内容我们将根据以上的问题展开讨论
使用text插件加载非js文件
你是否经常需要在js中拼凑各种html结构?
那么我推荐你使用前端的模版引擎。
你是否在纠结在RequireJS中如何加载一个html模版?因为看上去RequreJS好像只是一个JavaScript模块加载器,默认是
.js
后缀,那么我们如何让它同时支持文件加载?答案是:使用text插件
无需做任何的配置,只需将text.js放在和require.js相同的目录下,require就瞬间被丰富了,我们可以通过使用这样的方式:
require(["some/module", "text!some/module.html", "text!some/module.css"], function(module, html, css) { //the html variable will be the text //of the some/module.html file //the css variable will be the text //of the some/module.css file. });
text!
命令表示后面的模块是一个文件,那么RequireJS将把整个文件读入一个字符串后作为一个模块传入。或许我们还有一个特殊的需求——只读取这个文件body中的内容,那么text.js也帮我们做了:
require(["text!some/module.html!strip"], function(html) { //the html variable will be the text of the //some/module.html file, but only the part //inside the body tag. });
!strip
命令帮助我们快速的读取了我们真正需要的patial内容。
使用
strip
的好处是我们可以使用html进行模版的设计,在head中引入css文件,但是当它作为一个模版被加载时,可以忽略掉head中添加的样式文件的引用,因为这个样式文件往往是整个页面样式的集合。
在单页应用中使用RequireJS
在单页应用中,我们往往把配置 requirejs.config
方法和程序入口 require
方法放在同一个main.js文件中:
requirejs.config({ //By default load any module IDs from js/lib baseUrl: 'js/lib', //except, if the module ID starts with "app", //load it from the js/app directory. paths //config is relative to the baseUrl, and //never includes a ".js" extension since //the paths config could be for a directory. paths: { app: '../app' }});// Start the main app logic.require(['jquery', 'canvas', 'app/sub'],function ($, canvas, sub) { //jQuery, canvas and the app/sub module are all //loaded and can be used here now.});
在页面中通过以下方式调用:
<script data-main="scripts/main" src="scripts/require.js"></script>
上述代码中,浏览器加载require.js后,RequireJS会自动寻找data-main定义的入口,然后读取配置,接着执行require方法。
在多页应用中使用RequireJS
在多页应用中,RequireJS的使用没有太大的差别,我们可能碰到两个问题:
如何共用一套requirejs.config?
解决方案是把requirejs.config拆分到单独的js文件中,并且保证这个文件在main.js之前被加载,即保证require方法执行的时候,requirejs已经读取了目录的配置、模块的依赖等配置信息。
如何保证带Path信息的页面中baseUrl指向正确?
解决方案是在requirejs.config方法执行前定义一个module,这个module的作用是返回当前站点的root url,使用require来加载这个预定义的module,然后把这个root url配置到require的baseUrl中。
如在JAVA中,我们可以在Velocity模版中这样写:
<script type="text/javascript" src="$app/assets/js/require.js"></script><!-- Global variable module which will take variables from sever side --><script type="text/javascript"> define('global', { context: "$app" });</script><script type="text/javascript" src="$app/assets/js/base.js"></script>
可以注意一下上述define方法的简洁写法,因为我们之前介绍过在define的回调中必须有返回值。这个简洁写法中我们定义了名为global的模块,这个模块返回当前站点的root url
base.js中,我们通过require方法加载global模块:
require(['global'], function(global){ require.config({ waitSeconds:30, baseUrl: global.context + '/assets/js', //... path:{ //... }, shim:{ //... } });});
如此一来,即使页面的url中带有多个path信息,我们也可以保证requirejs中的js加载地址是正确的。
RequireJS插件
- 希望在文档加载完后再执行require的回调,我们可以使用domReady插件
- 希望构建多语言的应用,我们可以使用i18n插件
使用r.js进行文件合并压缩
RequireJS提供了r.js,供我们在发布产品时对代码进行合并压缩,r.js不仅仅适用于js和css文件,还可以打包整个工程。
具体使用非常简单,首先看一下调用的代码:
node r.js -o build.js
从上面的代码中我们可以看到,我们需要3样东西
- nodejs运行环境
- r.js
- build.js
第一步是安装NodeJS,只需要打开nodejs.org, 点击install就可以安装了,没有任何的依赖,需要注意的是安装的时候我们要有Admin权限
第二步是从RequireJS官网获取r.js
第三步是编辑build.js文件,build.js和require.config里面的内容几乎一模一样,来看一个Sample:
({ //需要压缩的工程的根目录 appDir: './', //压缩的时候需要忽略的一些文件,可以用正则表达式 fileExclusionRegExp: /^(r|build)\.js$/, //压缩css的方式 optimizeCss: 'standard', //压缩的工程会被拷贝到dir指定的目录中 //注意:压缩并不破坏原有的工程,而是把原有的工程拷贝到dir指定的目录下 dir: './dist', //需要压缩的模块入口 //这里我们有3个页面,对应modules中3个文件的3个require函数 modules: [ { name: '../Main' }, { name:'../HomePageMain' }, { name:'../Help' } ], baseUrl: 'js/common', paths: { model: '../model', view: '../view', template: '../template' }, shim: { 'jquery': { exports: '$' }, 'backbone': { deps: ['underscore', 'jquery', 'json2'], exports: 'Backbone' }, 'underscore': { exports: '_' }, 'json2': { exports: 'JSON' }, 'jquery-ui': { deps: ['jquery'], exports: '$' }, 'gmap3.min': { deps: ['jquery'] } }})
只有前5个参数和requirejs.config
中的不同,请看注释。
r.js的文件压缩功能需要调用NodeJS环境提供的方法,它做的是:
拷贝当前
appDir
(未压缩的工程目录)定义的目录下的所有文件到dir
(输出目录)定义的目录中, 拷贝过程中排除fileExclusionRegExp
定义的文件进入输出目录中,此时输出目录文件其实未经过压缩
根据modules定义的文件为入口, 分别寻找里面的require方法, 根据配置信息递归的读取依赖文件,使用uglify提供的功能进行文件的压缩,最后合并到当前module中
4 同时r.js会遍历目录下的所有文件资源(如css,html),对其进行压缩。
以Main.js模块为例,执行 r.js -o build.js
后的效果是,运行时只有一个Main.js,不再需要长时间的等待多个文件的异步加载,所有Main.js依赖的模块,包括模块依赖的html模板、js全部被压缩在Main.js中,所有页面的文件,包括css,都经过了gzip压缩。
这样做的好处是:
减少请求, 原来加载Main.js的时候需要递归加载所有依赖的文件,这个步骤在压缩后就不需要了,只有一个Main.js文件,并且代码经过混淆
提高响应速度,所有的文件经过gzip的方式压缩后,显著提高了传输效率
- 组件模块化最佳实践
- vue组件最佳实践
- vue组件最佳实践
- vue组件最佳实践
- vue组件最佳实践
- vue组件最佳实践
- vue组件最佳实践
- 组件模块化
- JAVA调用外部进程最佳实践版组件
- Web UI组件化最佳实践的思考
- 将React组件迁移到ES6最佳实践
- Android 组件化 —— 路由设计最佳实践
- Android 组件化 —— 路由设计最佳实践
- 最佳实践
- 最佳实践
- 最佳实践
- css 模块化实践
- 前端组件模块化
- spring-向collection注值
- Spring cloud Q&A
- C++ decltype类型说明符
- 使用 RPI.GPIO 模块的脉宽调制(PWM)功能
- android Toast初探
- 组件模块化最佳实践
- 静态持续变量、内部链接性
- Java中的递归思想
- Python3面向对象编程笔记(一)
- kendou grid 合并列和行
- 各种算法OID
- strpos、 strstr、 substr三个函数的对比讲解
- windows调试 -- 创建.dump文件
- qduoj 生化危机&&ycb老师的电脑中毒了(邻接表)