模块化的历史和模块加载器

模块化的历史和模块加载器

模块化的需求来源

  1. 前端代码日渐复杂,web应用越来越像桌面应用
  2. 复用别人的代码
  3. 更少的网络请求

http://huangxuan.me/js-module-7day/#/

实现模块化

基本原理

  1. 函数作用域
function m1(){
	// 模块内容
	// 这种方式污染全局变量
}
  1. 对象写法
var module1 = new Object({
	_count = 0;
	m1: function(){
	}
	// 模块成员会被暴露,内部状态可以被外部改写
})
  1. 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加载问题

  1. js文件加载的时候,页面会出现假死(使用defer或async就无法达到2中的要求)
  2. js文件的依赖关系需要通过script标签顺序去控制
  3. 如果模块过多且不打包,网络请求多

模块加载器解决的问题

  1. 实现js文件的异步加载,避免网页失去响应
  2. 管理模块之间的依赖关系,便于代码的编写和维护

模块加载器演变史

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,可以将模块打包,减少网络请求

http://www.hangge.com/blog/cache/detail_1704.html

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依赖懒执行,打日志可以看出来

https://blog.csdn.net/zshake/article/details/53054722

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]) => {
   ···
});

http://es6.ruanyifeng.com/#docs/module

打包工具

require.js有类似r.js的打包工具,此外还有一些打包工具

browserify

让浏览器加载Nodejs模块

$ browserify main.js -o bundle.js

https://javascript.ruanyifeng.com/tool/browserify.html

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

posted on 2018-09-04 21:16  wudipmd  阅读(577)  评论(0编辑  收藏  举报

导航