浏览器端模块化

525 阅读8分钟

概述

本文主要介绍了浏览器端模块化的四种模式:commonjs、AMD、ES6模块化。没有太多的理论,用实用的例子讲解,在写例子的过程中介绍每个部分的作用,个人感觉会更容易懂、实用一点。

不使用以上三种方式实现模块化

作为对比,如果不用以上的三种方式,我们怎么实现浏览器端模块化呢?

新建以下目录结构:

|-01_NO_AMD
    |-js //源码所在的目录
      |-alerter.js
      |-dataService.js
    |-app.js //应用主源文件
    |-test.html

test.html作为主页面,app.js作为页面的主js,引用了alerter.js, alerter.js又引用了dataService.js。

test.html的代码如下,需要注意的是文件引用的顺序,应该按照依赖要先引入的原则,例如alerter.js又引用了dataService.js,dataService.js的引用一定要写在alerter.js引用之前,否则会报错。

<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
  <div>
    <h1>Module demo 1:未使用AMD(require.js)</h1>
  </div>
  <script type="text/javascript" src="./js/dataService.js"></script>
  <script type="text/javascript" src="./js/alert.js"></script>
  <script type="text/javascript" src="./app.js"></script>
</body>
</html>
app.js的代码如下

通过一个立即执行执行函数在引用页面立即执行。

(function (alerter) {
  alerter.showMsg()
}
)(alerter)
alerter.js的代码如下
// 定义一个有依赖的模块
(function(window,dataService) {
  let msg = 'alert.js'
  function showMsg() {
    console.log(msg, dataService.getName())
  }
  window.alerter = {showMsg}
}
)(window, dataService)

我们通过注入window对象的方式,通过window对象把alerter中的showMsg中的方法暴露出去。

dataService.js的代码如下:
(function(window) {
  let name = 'dataservice.js'
  function getName() {
    return name
  }
  window.dataService = {getName}
})(window)

我们通过注入window对象的方式,通过window对象把dataService中的中的getName方法暴露出去。

本质是通过立即执行函数将要暴露的内容挂载到window对象的方式实现的模块化。

commonjs模块化

步骤:

一、新建一个文件夹作为我们的工程目录,比如就叫02_CommonJS-Browserify。

二、在03_CommonJS-Browserify下新建如下目录结构,其中packag.json添加自己定义的name和version属性。

|-js
    |-dist //打包生成文件的目录
    |-src //源码所在的目录
      |-module1.js // 模块一
      |-module2.js  //
      |-module3.js
      |-app.js //应用主源文件
  |-index.html
  |-package.json
    {
      "name": "browserify-test",
      "version": "1.0.0"
    }

三、下载browserify,需要全局安装再局部安装

首先介绍下browserify的作用,commonjs主要是针对服务器端(node)的。并不能直接应用于浏览器端,browserify的作用就是使得浏览器端也能使用commonjs来做模块化。(ps:从browserify的字面意思也能看出来是浏览器化的意思)

browserify需要先全局安装,再局部安装。

全局安装:npm install browserify -g

局部安装:进入当前的项目目录,本例中就是02_CommonJS-Browserify项目文件夹下,局部安装:npm install browserify --save-dev

我们看一下packge.json,其中的dependencies中的定义的运行依赖,devdependencies中定义的开发依赖。

node_modules是npm install安装产生各种依赖包。

四、定义模块代码

下面介绍三种子模块的导出方式:

module1.js
module.exports = {
  foo() {
    console.log('moudle1()')
  }
}
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(主js)

在主js文件app.js中通过require的方式引用以上定义并暴露出来的三个模块文件。注意,通过npm安装的外部插件,可以通过require(‘插件名称’)的方式引入。

//引用模块
let module1 = require('./module1')
let module2 = require('./module2')
let module3 = require('./module3')
// 引用外部插件
let uniq = require('uniq')
​
//使用模块
module1.foo()
module2()
module3.foo()
module3.bar()
​
console.log(uniq([1, 3, 1, 4, 3]))

这里的uniq是我们npn安装的外部插件,直接通过require('xxxx')的形式就可以引用。

写到这里,稍微停一下,问一个问题,我们应该在页面里面即:index.html中怎么引用。是直接引用app.js这样吗?大家可以试一下,直接把app.js引入到页面中。

<script type="text/javascript" src="js/src/app.js"></script> 

我们打开控制台,发现页面报错了。

是因为commonjs的引入方式想要应用于浏览器端,需要借助一个工具,就是我们刚刚安装的browserify,用browserify进行打包处理。

browserify打包处理js
browserify js/src/app.js -o js/dist/build.js

解释下上面的打包指令:browserify js/src/app.js表示我们要打包的入口文件地址,-o表示的output输出,js/dist/bundle.js表示我们打包后要生成的文件名称和路径。

这时,我们在主页面中,就要引用browserify后生成的文件build.js,这时再打开控制台,我们可以看到如我们所期待的coneole信息。

<script type="text/javascript" src="./js/dist/build.js"></script>

AMD

不同于commonjs,AMD是专门用于浏览器端,异步加载的模块化方式。

基本概念

定义没有依赖的模块

define(function(){
  return 模块
})

定义有依赖的模块

define([‘module1’,‘module2’],function(m1, m2)) {
  return 模块
}

引入使用模块

require(['module1','module2'],function(){
  使用'module1','module2'
})

实现例子

AMD模块化通过require.js实现,实现步骤如下

一、去require.js官网下载require.js,将require.js导入项目: js/libs/require.js

按照如下的目录结构新建文件

|-02_requireJs
    |-js //源码所在的目录
      |-module
        |-alerter.js
        |-dataService.js
      main.js
    |-index2.html

其中index2.html内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
</body>
<script data-main="js/main.js" src="js/libs/require.js"></script>
</html>

在index2.html文件中通过src="js/libs/require.js"引入require.js,通过data-main="js/main.js"引入主js文件。

main.js中的内容如下:

(function() {
  requirejs.config({
    baseUrl:'js/'
    paths: {
      dataService:'./modules/dataService',
      alert:'./modules/alert'
    }
  });
​
  requirejs(['alert'], function(alert){
    alert.showMsg()
  })
})()

这里的main.js主要做了两件事:

一是引入自己依赖的js文件:

requirejs(['alert'], function(alert){
    alert.showMsg()
  })

二是配置路径,所有被引用的依赖都在这里进行定义(有点类似vue-cli中的路由路径的定义)。

 requirejs.config({
    baseUrl:'js/'
    paths: {
      dataService:'modules/dataService', 
      alert:'modules/alert'
    }
  });

其中baseUrl表示根路径(也可以不填,不填的话可以通过相对路径的形式来引入)。path定义各个子模块的引用。例如在这里定义了dataService:'modules/dataService',这样我们在 alert.js中引用dataService.js就可以通过main.js中path的映射找到对应的模块文件。

还有一点要注意:文件的引入,不要带js后缀,否则浏览器会解析多带上.js,导致找不到文件,以dataService为例,会解析成dataService.js.js。

alert.js中的内容如下(有依赖文件):

// 定义有依赖的模块
define(['dataService'], function(dataService) {
  let msg = 'alert.js';
  function showMsg() {
    console.log(msg, dataService.getName())
  }
  // 暴露模块
  return {showMsg}
});
​

alert.js依赖的文件通过['dataService']形式添加,注意这里我们可以通过['dataService']找到依赖文件,就是因为我们在main.js中做了路径的配置。通过return {showMsg}的形式暴露出去,其实也有点类似我们上面通过window.alerter = {showMsg}的方式来暴露模块内容。

dataService.js文件中的内容如下

// 定义没有依赖的模块
define(function() {
  let name = 'dataService.js';
  function getName() {
    return name
  }
  // 暴露模块
  return {getName}
});

ES6

基本概述

说明:

依赖模块需要编译打包处理

语法:

导出模块:export

引入模块:import

实现:

使用Babel将ES6编译为ES5代码

使用browserify编译打包js使其能应用于浏览器端

Babel的官网:babeljs.cn

定义以下的目录结构文件

|-04_ES6_Babel_Browserify
    |-js //源码所在的目录
      |-dist
        |-bundle.js
      |-lib
      |-src
        |-module1.js
        |-module2.js
        |-module3.js
    |-index.html

1、定义package.json文件,定义name和version

  "name": "es6-babel-browserify",
  "version": "1.0.0",

2、安装babel-cli,bebel-preset-es2015和browserify(ps:cli全称 command line interface)

npm install babel-cli browserify -g

npm install babel-preset-es2015 --save-dev

3、定义.babelrc文件(rc表示run control),内容如下,这里的presets(预设)表示babel要做的将es2015(es6)转换成es5的操作。注意babel可以做很多的工作,除了将es6转es5外,还有例如对.vue文件的编译,对react中jsx文件的编译等

{
 "presets": ["es2015"]
 }

4、编码

通过三种不同export暴露模块方式定义三个子模块

js/src/module1.js

export function foo() {
  console.log('module1 foo()');
}
export let bar = function () {
  console.log('module1 bar()');
}
export const DATA_ARR = [1, 3, 5, 1]

js/src/module2.js

let data = 'module2 data'function fun1() {
  console.log('module2 fun1() ' + data);
}
​
function fun2() {
  console.log('module2 fun2() ' + data);
}
​
export {fun1, fun2}

js/src/module3.js

export default {
  msg:'默认暴露',
  foo() {
    console.log(this.msg)
  },
  foo2() {
    console.log('我是默认暴露的对象')
  }
}

定义主js文件app.js,通过import的形式引入以上的几个子模块

js/src/app.js

import {foo, bar} from './module1'
import {fun,fun2} from './module2'
import module3 from './module3'
foo()
bar()
fun()
fun2()
module3.foo2()

我们看到module1和module2分别采用了单独暴露和统一暴露的形式,这两种形式有一个特点是要求引入的时候必须要用结构赋值的这种形式,那有没有不用结构赋值的暴露方式呢?有的。通过默认暴露的方式,引入的时候就无需采用结构赋值的形式,module3中就是采用默认形式。

注意默认暴露形式智能写一个export,下面的写法就是错误的,会报错。

// 错误示范,只能写一次export
export default ()=> {
  console.log('我是默认暴露')
}
export default {
  msg:'默认暴露',
  foo() {
    console.log(this.msg)
  }
}

5、编译

  • 使用Babel将ES6编译为ES5代码(但包含CommonJS语法) 。babel js/src -d js/lib,babel js/src表示的是用Babel将src文件夹下的所有js都进行Babel编译,js/lib表示编译生成的结果文件。注意,这里的lib文件夹babel js/src -d js/lib指令会帮我们自动生成,无需手动创建。到这里先停一下,去lib文件夹下线看一下打包生成的结果文件,文件目录结果与打包前保持一致,里面又有我们熟悉的commonjs的require,这时我们可以试一下直接把lib中的main.js引用到html文件,会包require未定义,我们在commonjs中也遇到过,这里我们可以理解成我们要开始用commonjs模块化啦,我们需要和commonjs一样用Browserify使得模块化规范适用于浏览区端。

  • 使用Browserify编译js命令如下 :

    browserify js/lib/app.js -o js/dist/bundle.js,
    

    这里的bundle.js文件需要我们手动创建(区别于babel命令babel js/src -d js/lib能够自动生成lib文件夹)

  • index.html中引入打包后的bungle.js

      <script type="text/javascript" src="js/dist/bundle.js"></script>
    
  • 打开index.html页面,查看控制台输出。