JavaScript模块化规范详解
目录
1. 为什么要模块化?
-
Web sites are turning into Web Apps.
-
Code complexity(复杂度) grows as the site gets bigger.
-
Highly decoupled(解耦) JS files/modules is wanted.
-
Deployment(部署) wants optimized(优化) code in few HTTP calls.
2. 模块化的好处
-
避免命名冲突(减少命名空间污染)
-
更好的分离,按需加载
-
更高复用性
-
高可维护性
3. 页面引入加载script存在的问题:
-
请求过多
-
依赖模糊
-
难以维护
4. 模块化规范
4.1 CommonJS
每个文件都可当做一个模块
-
在服务器端: 模块的加载是运行时同步加载的。
-
在浏览器端: 模块需要提前编译打包处理。
基本语法:
- 暴露模块:
module.exports = value
exports.xxx = value
- 引入模块:
require(xxx)
- 第三方模块: xxx为模块名
- 自定义模块: xxx为模块文件路径
实现:
- 服务器端实现: Node.js
- 浏览器端实现: Browserify,也称为CommonJS的浏览器端的打包工具。
4.1.1 Node.js模块化过程
-
安装Node.js
-
创建项目结构
|-modules |-module1.js |-module2.js |-module3.js |-app.js |-package.json { "name": "commonJS-node", "version": "1.0.0" }
-
下载第三方模块
npm install uniq --save
-
模块化编码
-
module1.js
module.exports = { foo() { console.log('module1 foo()'); } };
-
module2.js
module.exports = function() { console.log('module2()'); };
-
module3.js
exports.foo = function() { console.log('module3 foo()'); }; exports.bar = function() { console.log('module3 bar()'); };
-
app.js
/* * 1. 定义暴露模块: * module.exports = value; * exports.xxx = value; * 2. 引入模块: * var module = require(模块名或模块路径); */ // 引用模块 let fs = require('fs'); // fs是nodejs中内置的文件系统模块 let uniq = require('uniq'); // 下载的第三方模块,功能是数组排序去重 let module1 = require('./modules/module1'); // 自定义的module let module2 = require('./modules/module2'); let module3 = require('./modules/module3'); // 使用模块 module1.foo(); module2(); console.log(uniq([1, 3, 2, 4, 2])); fs.readFile('app.js', function(error, data){ console.log(data.toString()); })
-
-
通过node运行app.js:
node app.js
4.1.2 Browserify模块化过程
由于浏览器端不具备node那样的环境,不能识别require
等方法,所以浏览器端的模块化需要借助Browserify
工具来完成打包,以便浏览器识别。
-
创建项目结构
|-js |-dist // 打包生成文件的目录 |-src // 源码所在的目录 |-module1.js |-module2.js |-module3.js |-app.js // 应用主源文件 |-index.html |-package.json { "name": "browserify-test", "version": "1.0.0" }
-
下载browserify
- 全局:
npm install browserify -g
// 全局环境安装 - 局部:
npm install browserify --save-dev
// 只是帮助我们编译打包文件,在开发环境(-dev)下安装即可,将来生产环境并不需要
安装完后,package.json中会变成:
{ "name": "browserify-test", "version": "1.0.0" "devDependencies": {"browserify": 版本号}, // 开发环境下的依赖包 "dependencies": {"uniq": 版本号} // 全局依赖包 }
- 全局:
-
定义模块代码
和Node.js中一样的定义。
-
打包处理js源文件:
-
browserify js/src/app.js -o js/dist/build.js
-
上面的命令执行完毕后,生成了打包后的文件,就可以在html页面中引入了:
<script type="text/javascript" src="js/dist/build.js"></script>
-
4.2 AMD
Asynchronous Module Definition(异步模块定义)
专门用于浏览器端,模块的加载是异步的。
语法:
-
定义暴露模块
-
定义没有依赖的模块
define(function(){ return 模块 })
-
定义有依赖的模块
define(['module1','module2'], function(m1, m2){ return 模块 })
-
-
引入使用模块
require(['module1','module2'], function(m1, m2){ //显式声明依赖注入 使用m1/m2 })
实现: Require.js
在没有使用AMD规范(require.js)的时候,我们通过多个script标签来按照依赖顺序依次引入js文件,
这样不但增加了HTTP请求数,更增加了维护的难度,容易出错。
通过模块加载器require.js
来加载js模块:
-
下载Require.JS,官网: http://www.requirejs.cn/
-
创建项目结构
|-js |-libs |-require.js |-modules |-module1.js |-module2.js |-main.js |-index.html
-
定义模块代码
-
module1.js
define(function(){ let msg = 'hello'; function getMsg(){ return msg.toUpperCase(); } return {getMsg}; });
-
module2.js
define(['module1', 'jquery'], function(m1, $){ let name = "module2"; function showMsg(){ $('body').css('background', "red"); alert(m1.getMsg() + ',' + name); } return {showMsg}; });
-
-
编写应用主入口: main.js
(function () { // 配置 require.config({ //基本路径 baseUrl: "js/", //模块标识名与模块路径映射 paths: { "module1": "./modules/module1", // 内部会给路径自动加上.js扩展名 "module2": "./modules/module2", } }); // 引入使用模块 require(['module2'], function(module2){ module2.showMsg(); }) })()
-
页面使用模块
<script data-main="js/main.js" src="js/libs/require.js"></script>
require.js 在加载的时候会检查
data-main
属性:
可以在data-main指向的脚本中设置模板加载 选项,然后加载第一个应用模块。注意:你在main.js中所设置的脚本是异步加载的。所以如果你在页面中配置了其它JS加载,则不能保证它们所依赖的JS已经加载成功。
例如:
<script data-main="scripts/main" src="scripts/require.js"></script> <script src="scripts/other.js"></script>
// contents of main.js: require.config({ paths: { foo: 'libs/foo-1.1.3' } });
// contents of other.js: // This code might be called before the require.config() in main.js // has executed. When that happens, require.js will attempt to // load 'scripts/foo.js' instead of 'scripts/libs/foo-1.1.3.js' require( ['foo'], function( foo ) { });
-
使用第三方基于require.js的框架(jquery)
jQuery支持AMD规范,在源码的最后几行,
define("jquery", [], function(){return jQuery;});
这说明jQuery暴露了一个模块接口,并且标识名为jquery。注意在引入的时候,写
jquery
而不是jQuery
。将jQuery库文件导入到项目的libs目录中,然后在main.js中配置jquery路径:
path: { 'jquery': './libs/jquery-1.10.1' }
接下来就可以使用在module中了。
define(['module1', 'jquery'], function (module1, $) { var name = 'Tom'; function showMsg() { $('body').css({background : 'red'}) alert(name + ' ' + module1.getMsg()) } return {showMsg} })
-
使用第三方不基于require.js的框架(Angular)
将
angular.js/angular-ui-router.js/angular-message
等导入项目目录,然后在paths
中添加angular
路径。为了配置不兼容AMD的模块,需要在
require.config
中多添加:shim: { 'angular': { exports: 'angular' }, `angular-message`: { exports: 'angular-message', deps: ['angular'] } }
如:
require.config({ //选择基础目录 baseUrl: 'src/', //别名 paths: { "angular": "lib/angular", "angular-ui-router": "lib/angular-ui-router" }, //需要声明paths中元素暴露的接口和依赖(angular内置没有支持AMD) shim: { 'angular': {exports: 'angular'}, 'angular-ui-router': {deps: ['angular']} } }); //核心入口 require(['angular', 'app-routes'], function(angular){ angular.element(document).ready(function(){ //angular.bootstrap是一个方法,表示将模块绑定给某个元素 //这里相当于自动给html标签添加了一个ng-app属性,值为app //类似Vue中$mount挂载的概念 angular.bootstrap(document, ['app']); //angular.element(document).find("html").addClass("ng-app"); }); });
4.3 CMD
专门应用于浏览器端,模块的加载是异步的。
实现: sea.js
,github: https://github.com/seajs/seajs
模块使用时才会加载执行。
语法:
-
定义暴露模块
-
定义没有依赖的模块
define(function(require, exports, module){ exports.xxx = value; module.exports = value; })
-
定义有依赖的模块
define(function(require, exports, module){ //引入依赖模块(同步) var module2 = require('./module2'); //引入依赖模块(异步),注意异步的function会在主线程执行完毕再执行,因此输出顺序可能变化 require.async('./module3', function(m3){ }) //暴露模块 exports.xxx = value; })
-
-
引入使用模块
define(function(require){ var m1 = require('./module1'); var m4 = require('./module4'); m1.show(); m4.show(); })
CMD规范,定义模块类似AMD,暴露模块类似Commonjs。
使用方法:
-
下载sea.js并引入到libs。
-
创建项目结构
|-js |-libs |-sea.js |-modules |-module1.js |-module2.js |-module3.js |-module4.js |-main.js |-index.html
-
定义sea.js的模块代码
-
module1.js
define(function(require, exports, module) { var data = "hello"; function show(){ console.log('module1 show()' + data); } // 向外暴露 exports.show = show; })
-
主入口模块: main.js
define(function(require){ var m1 = require('./module1'); m1.show(); })
-
-
在index页面引入
<script type="text/javascript" src="js/libs/sea.js"></script> <script type="text/javascript"> seajs.use('./js/modules/main.js') </script>
4.4 ES6模块化规范
ES6中内置了js模块化的实现,静态加载模块(编译时加载)。
语法:
-
定义暴露模块:
export
-
暴露一个对象(默认暴露):
export default 对象
可以暴露任意数据类型,暴露什么就接收到什么。
-
暴露多个对象(常规暴露):
// 分别暴露 export var xxx = value1; export let yyy = value2; // 统一暴露 var xxx = value1; let yyy = value2; export {xxx, yyy}
统一暴露或者分别暴露,在引入的时候必须用对象解构赋值的形式。
-
-
引入使用模块:
import
-
默认暴露的模块:
import xxx from '模块路径/模块名'
-
其他模块
import {xxx, yyy} from '模块路径/模块名' import * as module1 from '模块路径/模块名'
-
ES6模块是静态加载(编译时加载):import {} from 'xxx'
CommonJS
则是运行时加载:const {} = require('xxx')
问题:
一些浏览器还不能直接识别ES6模块化的的语法。
可以使用Babel
来将ES6转换为ES5,如果内部还使用了CommonJS
,浏览器仍然不能直接执行。
需要再次使用Browserify
来将文件打包处理,最终引入页面,浏览器可以直接运行。
js编译转换及打包方法:
-
定义package.json文件
{ "name": "es6_babel_browserify-test", "version": "1.0.0" }
-
安装
babel-cli
,babel-preset-env
和browserify
npm install babel-cli browserify -g npm install babel-preset-env --save-dev
-
定义
.babelrc
配置文件(babel在执行之前会先读取该文件){ "presets": [ [ "env", //所使用的preset名称 { "targets": { "node": "current" //兼容当前系统版本的node } } ] ] }
-
编写模块
-
js/src/module1.js
something...
-
js/src/app.js
import {fun1, fun2} from './module1'; import $ from 'jquery'; $('body').css('background', 'red'); fun1(); fun2();
-
-
编译打包
-
使用Babel编译为ES5语法(包含CommonJS):
babel js/src -d js/build
(可以自动生成新目录build) -
使用Browserify打包js:
browserify js/build/app.js -o js/dist/bundle.js
(不能自动生成dist目录)
-
-
页面中引入
<script type="text/javascript" src="js/build/bundle.js"></script>
-
引入第三方模块(jQuery)
-
下载jQuery模块:
npm install jquery@1 --save
(模块后加@代表下载相应版本号下的最新版本) -
在app.js中引入使用:
import $ from 'jquery'
-
当改变了模块中的代码后,需要重新转换(Babel)、编译打包(Browserify),再引入页面。
nodejs中的编译转换:
只需用到babel
,大致和浏览器端一样,需要注意的是,如果babel-cli
模块并未全局安装(-g
),而是开发环境依赖式安装(--save-dev
),那么可以在package.json
的scripts
中编写命令来运行babel
。
{
"name": "babel-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
// 如果直接写src,会自动寻找到其下的index.js文件并执行
"dev": "babel-node src --presets env",
// 用rimraf清空build目录,再将src目录中的代码编译转换并存放到build目录
"build": "rimraf build && babel src -d build --presets env"
},
"author": "",
"license": "ISC",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0",
"rimraf": "^2.6.3"
}
}
然后npm run dev
即可看到src/index.js
经过babel
编译后并执行的结果,npm run build
则是将src
下的代码都编译转换,放到build
目录下,以便发布与迁移。
小技巧:
在node
开发时为了实时预览代码执行结果,而不用反复运行npm run dev
,可以借助nodemon
,这是一个用来监视node.js应用程序中的任何更改并自动重启服务的模块。
安装:
npm install nodemon -D
在package.json
的scripts
中配置:
"dev": "nodemon -w src --exec \"babel-node src --presets env\"",
这样当运行npm run dev
就实时监控了src
目录下的文件变化,并自动执行变化后的代码。
注意:
前面使用到的babel-cli
只是一个命令行工具,某些时候我们会发现,将代码编译转换输出到build
目录后,使用node build
来运行,会报错regeneratorRuntime is not defined
,这是因为原代码中包含了generator
等一些较新的语法特性,即使经过babel
转换,还是由于欠缺polyfill
而无法运行。
此时我们就需要使用到babel
运行环境(模拟ES2015+环境)以及相关的一些插件,来让babel
应用到项目的生产环境。
安装:
npm install babel-plugin-transform-runtime -D
npm install babel-runtime -S
配置.babelrc
:
{
"presets": [
[
"env",
{
"targets": {
"node": "current"
}
}
]
],
"plugins": [
[
"transform-runtime",
{
"polyfill": false,
"regenerator": true
}
]
]
}
配置完毕后再执行npm run build
,然后就可以node build
顺利执行编译后的代码了。其实之前用到的babel-node
命令在执行代码时会自动加载polyfill
,但是一般只在开发环境下使用。
babel-polyfill
和 babel-runtime
是达成同一种功能(模拟ES2015环境,包括global keywords
,prototype methods
,都基于core-js
提供的一组polyfill
和一个regenerator runtime
)的两种方式,具体区别参考官方文档。
详细操作及相关配置请见官方文档。