模块化的历史和模块加载器
模块化的需求来源
- 前端代码日渐复杂,web应用越来越像桌面应用
- 复用别人的代码
- 更少的网络请求
实现模块化
基本原理
- 函数作用域
function m1(){
// 模块内容
// 这种方式污染全局变量
}
- 对象写法
var module1 = new Object({
_count = 0;
m1: function(){
}
// 模块成员会被暴露,内部状态可以被外部改写
})
- IIFE
var module1 = (function($){
var _count = 0;
var m1 = function(){
// ...
}
var _$body = $('body');
var foo = function(){
console.log(_$body);
}
return {
m1: m1,
foo: foo
}
//
})(jQuery)
module1.foo();
模块加载器
js加载问题
- js文件加载的时候,页面会出现假死(使用defer或async就无法达到2中的要求)
- js文件的依赖关系需要通过script标签顺序去控制
- 如果模块过多且不打包,网络请求多
模块加载器解决的问题
- 实现js文件的异步加载,避免网页失去响应
- 管理模块之间的依赖关系,便于代码的编写和维护
模块加载器演变史
require.js(AMD规范)
使用require.js并指定入口文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="js/require.js" data-main="js/main"></script>
</body>
</html>
// 主模块(入口文件)的写法
require.config({
paths: {
"jquery": 'jquery.min',
"underscore": 'underscore.min',
"backbone": 'backbone.min'
}
});
require(['jquery', 'underscore','backbone'], function($, _, Backbone){
console.log(_.min([1,10,2]))
});
require(['math'], function (math) {
alert(math.min([10, 11]))
});
// 定义模块的方法
define(['underscore'], function(_){
var min = function (x, y){
return _.min(x,y);
};
return {
min: min
}
});
此外,require.js提供了一个优化工具r.js,可以将模块打包,减少网络请求
require.js还有一些插件,可以将图片和文本打包
sea.js(CMD规范)
// 引入sea.js并且配置入口文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">dfslljdslf</div>
<script src="../sea-modules/seajs/seajs/2.2.0/sea.js"></script>
<script>
seajs.config({
base: '../sea-modules/',
alias: {
'jquery': 'jquery/jquery/1.10.1/jquery.js'
}
})
seajs.use('../static/test/main.js');
</script>
</body>
</html>
// 入口文件写法
define(function (require, exports, module) {
var test = require('./test');
test.fadeOut();
});
// 定义模块方法
define(function(require, exports, module){
var $ = require('jquery');
exports.fadeOut = function(){
$('#app').fadeOut();
}
})
区别
写法上:require.js依赖前置,sea.js依赖就近,语法更像common.js
加载和执行顺序上:require.js依赖提前执行,sea.js依赖懒执行,打日志可以看出来
common.js(node.js服务器端)
// 最终导出的是exports对象
console.log("example.js");
exports.message = "hi";
exports.say = function (){
console.log("hello");
};
// 使用require加载模块
var example = require('./example.js');
ES6 module
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
// CommonJS模块
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
// ES6模块
import { stat, exists, readFile } from 'fs';
上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
// export写法
// 1.直接导出
export var firstName = 'Michael';
// 2.先声明后导出
var firstName = 'Michael';
export {firstName}
// 3.导出函数或类
export function multiply(x,y){
return x+y;
}
// 4.export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系
export 1 // 报错
var m = 1;
export m; //报错
export var m = 1; // 不报错
export {m} // 不报错
// import写法
import {firstName, lastName, year} from './profile.js';
import { lastName as surname } from './profile.js';
// js后缀可以省略
import {myMethod} from 'util';
// import命令具有提升效果,会提升到整个模块的头部,首先执行
foo();
import { foo } from 'my_module';
// 由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构
import { 'f' + 'oo' } from 'my_module'; // 报错
// import语句会执行所加载的模块,因此可以有下面的写法
import 'lodash'; // 仅仅执行lodash模块,但是不输入任何值
// 模块的整体加载
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
// export default用法
// export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default'; // 不用括号
customName(); // 'foo'
// 一个模块只能有一个默认输出,因此export default命令只能使用一次
import()方法
在语法上,条件加载就不可能实现。如果import命令要取代 Node 的require方法,这就形成了一个障碍。因为require是运行时加载模块,import命令无法取代require的动态加载功能。
const path = './' + fileName;
const myModual = require(path);
上面的语句就是动态加载,require到底加载哪一个模块,只有运行时才知道。import命令做不到这一点。
因此,有一个提案,建议引入import()函数,完成动态加载。
import(specifier)
上面代码中,import函数的参数specifier,指定所要加载的模块的位置。import命令能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载。
import()返回一个 Promise 对象。下面是一个例子。
const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。
适用场景
- 按需加载
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
- 条件加载
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
- 动态模块路径
import(f()) // 根据函数f的返回结果,加载不同的模块
.then(...);
注意点
import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。
// 解构赋值
import('./myModule.js')
.then(({export1, export2}) => {
// ...·
});
// 同时加载多个模块
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});
打包工具
require.js有类似r.js的打包工具,此外还有一些打包工具
browserify
让浏览器加载Nodejs模块
$ browserify main.js -o bundle.js
webpack
可以打包各种文件资源,可支持各种模块规范
webpack.config.js配置文件
配置文件本身就是一个模块
var path = require('path');
var webpack = require('webpack');
var devFlagPlugin = new webpack.DefinePlugin({
_DEV_: JSON.stringify(JSON.parse(process.env.DEBUG || 'false'))
});
module.exports={
entry: { // 入口文件,String/Array/Object
bundle1: 'main1.jsx',
bundle2: 'main2.jsx',
bundle: 'main.jsx'
},
output:{ // 指定输出位置和文件名
filename:'[name].js',
path: path.resolve(__dirname, './')
},
mode:'development',
module: { // loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)
rules: [
{
test: /.jsx?$/,
exclude: /node_modules/,
use:{
loader: 'babel-loader', // 处理ES6
options: {
presets: ['es2015', 'react']
}
}
},
{
test: /.css$/, // 处理css文件
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
options: {
modules: true,
}
}
]
},
{
test: /.(jpg|png)$/, // 解析图片模块
use: {
loader: 'url-loader',
options: {
limit: 8192
}
}
}
]
},
plugins: [ // loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务,需要通过使用 new 操作符来创建它的一个实例
devFlagPlugin, // 根据node命令行参数设置变量_DEV_,_DEV_可以在任何模块使用
new webpack.optimize.CommonsChunkPlugin({ // code splitting
name: 'commons',
filename: 'commons.js',
})
]
};
// main.js
const React = require('react');
const ReactDOM = require('react-dom');
var style = require('./app.css'); // 导入css文件模块(css module)
var img1 = new Image();
img1.src = require('./big.png'); // 导入图片资源
document.body.appendChild(img1);
if(_DEV_){
document.write(new Date());
}
ReactDOM.render(
<div>
<h1 className={style.h1}>Hello, world!</h1>
<h2 className="h2">Hello, webpack</h2>
</div>,
document.querySelector('#wrapper')
);
webpack.config.js还可以导出一个函数或promise对象,可以导出多种规范的模块,可以热替换,只更新修改的模块。
webpack 从命令行或配置文件中定义的一个模块列表开始,处理你的应用程序。 从这些入口起点开始,webpack 递归地构建一个依赖图,这个依赖图包含着应用程序所需的每个模块,然后将所有这些模块打包为少量的 bundle - 通常只有一个 - 可由浏览器加载。
https://www.webpackjs.com/concepts/
https://github.com/ruanyf/webpack-demos#demo10-code-splitting-source
http://www.ruanyifeng.com/blog/2012/10/javascript_module.html
http://www.ruanyifeng.com/blog/2012/10/asynchronous_module_definition.html
http://www.ruanyifeng.com/blog/2012/11/require_js.html