JavaScript 模块封装

前言介绍

  在最早的时候JavaScript这门语言其实是并没有模块这一概念,但是随着时间的推移与技术的发展将一些复用性较强的代码封装成模块变成了必要的趋势。

  在这篇文章中主要介绍原生的 JavaScript封装的几种手段以及新增的 ES6 Module的语法,来实现模块封装。

  并且会简单的使用WebpackEs6代码向后兼容。

引入问题

  以下有两个Js文件,如果不采取任何封装手段直接导入会导致window环境污染。

  并且,如果文件中有相同名字的变量或函数会发生命名冲突,因为它们都是放在全局作用域window对象中的。

<script src="./js_m1.js"></script>
<script src="./js_m2.js"></script>
<script>"use strict";

    // 这是由于js_m2后引入,所以js_m1的同名变量以及函数都被覆盖掉了。

    console.log(module_name);  // js_m2

    show();  // js_m2.show
</script>
var module_name = "js_m1";

function show(){
        console.log("js_m1.show");
}
var module_name = "js_m2";

function show(){
        console.log("js_m2.show");
}

image-20200807115438551

简单解决

IIFE封装

  针对上述问题,采取函数的闭包及作用域特性我们为每个模块封装一个作用域。

  第一步:进行自执行函数包裹代码封装出局部作用域

  第二步:向外部暴露接口,为window对象添加新的对象

<script src="./js_m1.js"></script>
<script src="./js_m2.js"></script>
<script>"use strict";

    console.log(js_m1.module_name);  // js_m1

    js_m1.show();  // js_m1.show

    console.log(js_m2.module_name);  // js_m2

    js_m2.show();  // js_m2.show
</script>
(function () {

        var module_name = "js_m1";

        function show() {
                console.log("js_m1.show");
        }

        window.js_m1 = { module_name: module_name, show: show };
        // 在es6中,可简写为 { module_name , show }
}())
(function () {

        var module_name = "js_m2";

        function show() {
                console.log("js_m2.show");
        }

        window.js_m2 = { module_name: module_name, show: show };  
        // 在es6中,可简写为 { module_name , show }
}())

image-20200807120606342

Es6块级封装

  Es6之前,由于没有出现块级作用域的概念,那时候大家都使用上面的方式进行封装。

  在当Es6的块级作用域出现之后,又诞生出了新的封装方式即块级作用域封装。

  IIFE封装相同,都是利用作用域的特性进行封装。

  注意一点,块级作用域只对letconst声明有效。

<script src="./js_m1.js"></script>
<script src="./js_m2.js"></script>
<script>"use strict";

    console.log(js_m1.module_name);  // js_m1

    js_m1.show();  // js_m1.show

    console.log(js_m2.module_name);  // js_m2

    js_m2.show();  // js_m2.show
</script>
{
        let module_name = "js_m1";

        let show = function () {
                console.log("js_m1.show");
        }

        window.js_m1 = { module_name, show };

}
{
        let module_name = "js_m2";

        let show = function () {
                console.log("js_m2.show");
        }

        window.js_m2 = { module_name, show };

}

image-20200807121327601

Es6 module 语法

  上面的两种方式虽然都能达到模块封装的效果,但是我们依然有更好的选择。

  下面将介绍极力推荐的Es6 module语法进行导入。

  学习Es6 module从以下三个方面来入手:

  1.模块标签及其特性

  2.导出

  3.导入

模块标签

  要想使用Es6 module语法导入模块,必须使用模块标签来引入Js文件。

  模块标签与普通的<script>标签具有一些不太一样的地方,下面会从各个方面逐一进行介绍。

声明标签

  <script>标签添加上type="module"的属性。

<script type="module"></script>

导入路径

  在浏览器中引用模块必须添加路径如./ ,但在打包工具如webpack中则不需要,因为他们有自己的存放方式。

  总而言之,即使是在当前目录也要添加上./,不可以进行省略。

  这也是推荐的一种引入文件方式,不管是何种语言中都推荐引入文件时不进行路径省略。

  正确的导入路径

<script type="module" src="./js_m1.js"></script>
<script type="module" src="./js_m2.js"></script>

  错误的导入路径

<script type="module" src="js_m1.js"></script>  // 不可省略!省略就会抛出异常
<script type="module" src="js_m2.js"></script>

延迟解析

  所谓延迟解析是指在模块标签中的代码会提到HTML代码以及嵌入式的<script>标签后才进行执行。

  注意看下面的示例,编码时模块标签在普通的<script>之上,但是结果却相反。

<script type="module">

    console.log("<script type='module'> code run...");

</script>

<script>

    "use strict";

    console.log("<script> code run...");

</script>

image-20200807123803291

严格模式

  模块标签中的所有代码都是按严格模式运行的,请注意变量名的声明以及this指向问题,同时还有解构赋值等等。

<script type="module">

    username = "云崖";  // 抛出异常,未声明

</script>
<script type="module">

    let obj = {
        show() {
            console.log(this); // {show: ƒ}
            (function () { console.log(this); }())  // undefined 严格模式下为undefined ,普通模式下为window对象
        }
    };

    obj.show();

</script>

作用域

  每个模块标签中的代码都会为其创建一个专属的作用域,禁止相互之间进行访问。

  而普通的<script>标签中的代码全部在全局作用域下执行。

<script>

    let m1 = "m1...";

</script>

<script>

    console.log(m1);  // m1...

</script>
<script type="module">

    let m1 = "m1...";

</script>

<script type="module">

    console.log(m1);  // Uncaught ReferenceError: m1 is not defined

</script>

预解析

  模块在导入时只执行一次解析,之后的导入不会再执行模块代码,而使用第一次解析结果,并共享数据。

  可以在首次导入时完成一些初始化工作

  如果模块内有后台请求,也只执行一次即可

<script type="module" src="./js_m3.js"></script>
<script type="module" src="./js_m3.js"></script>
<script type="module" src="./js_m3.js"></script>

<!-- 导入多次,也只执行一次代码  -->
<!-- 打印结果如下:import m3... -->

<!-- js_m3内容如下:

    console.log("import m3...");

-->

导出模块

  ES6使用基于文件的模块,即一个文件一个模块。

  可以使用export来将模块中的接口进行导出,导出方式分为以下几种:

  1.单个导出

  2.默认导出

  3.多个导出

  4.混合导出

  5.别名导出

  另外,ES6的导出是是以引用方式导出,无论是标量还是对象,即模块内部变量发生变化将影响已经导入的变量。

单个导出

  下面将使用export来将模块中的接口进行单个单个的导出。

export let module_name = "js_m3.js";

export function test(){
        console.log("测试功能");
}

export class User{
        constructor(username){
                this.username = username;
        }

        show(){
                console.log(this.username);
        }
}

默认导出

  一个模块中,只能默认导出一个接口。

  如果默认导出的是一个类,那么该类就可以不用起类名,此外函数同理。

export let module_name = "js_m3.js";

export function test(){
        console.log("测试功能");
}

export default class{  // 默认导出
        constructor(username){
                this.username = username;
        }

        show(){
                console.log(this.username);
        }
}

多个导出

  可以使用exprot{}的形式进行接口的批量多个导出。

let module_name = "js_m3.js";

function test() {
        console.log("测试功能");
}

class User {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name, test, User };

混合导出

  使用export default 导出默认接口,使用 export {} 批量导入普通接口

let module_name = "js_m3.js";

function test() {
        console.log("测试功能");
}

export default class {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name, test };

  同时也可以使用as来为一个导出的接口取别名,如果该接口别名为default则将该接口当做默认导出。

let module_name = "js_m3.js";

function test() {
        console.log("测试功能");
}

class User {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name, test, User as default };

别名导出

  使用as来为导出的export {}中的导出接口起一个别名,当导入时也应该使用导出接口的别名进行接收。

  当一个接口的别名为default时,该接口将当做默认导出。

let module_name = "js_m3.js";

function test() {
        console.log("测试功能");
}

class User {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name as m_name, test as m_tst, User as default };

导入模块

  使用importfrom进行静态的模块的导入,注意导入时必须将导入语句放在顶层。

  模块的导入分为以下几部分:

  1.具名导入

  2.批量导入

  3.默认导入

  4.混合导入

  5.别名导入

  6.动态导入

具名导入

  具名导入应该注意与导出的接口名一致。

  下面是模块导出的代码:

let module_name = "js_m3.js";

function test() {
        console.log("测试功能");
}

class User {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name, test, User };

  使用具名导入:

<script type="module">

    import { module_name, test, User} from "./js_m3.js"; 

    console.log(module_name);  // js_m3.js

    test(); // 测试功能

    let u1 = new User("云崖");  

    u1.show(); // 云崖

</script>

批量导入

  如果导入的内容过多,可使用*进行批量导入,注意批量导入后应该使用as来取一个别名方便调用。

  下面是模块导出的代码:

let module_name = "js_m3.js";

function test() {
        console.log("测试功能");
}

class User {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name, test, User };

  使用批量导入:

<script type="module">

    import * as m3 from "./js_m3.js";   // 别名为m3,下面使用都要以m3开头

    console.log(m3.module_name);  // js_m3.js

    m3.test(); // 测试功能

    let u1 = new m3.User("云崖");  

    u1.show(); // 云崖

</script>

默认导入

  使用默认导入时不需要用{}进行接收,并且可以使用任意名字来接收默认导出的接口。

  下面是模块导出的代码:

let module_name = "js_m3.js";

function test() {
        console.log("测试功能");
}

class User {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name, test, User as default };

  使用默认导入,我们只导入默认导出的接口,可以随便取一个名字。

<script type="module">

    import m3U from "./js_m3.js";  

    let u1 = new m3U("云崖");  

    u1.show(); // 云崖

</script>

混合导入

  当一个模块中导出的又有默认导出的接口,又有其他的导出接口时,我们可以使用混合导入。

  使用{}来接收其他的导出接口,对于默认导出的接口而言只需要取一个名字即可。

  下面是模块导出的代码:

let module_name = "js_m3.js";

function test() {
        console.log("测试功能");
}

class User {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name, test, User as default };

  使用混合导入:

<script type="module">

    import m3U, { module_name, test } from "./js_m3.js";  

    console.log(module_name);  // js_m3.js

    test();  // 测试功能

    let u1 = new m3U("云崖");

    u1.show(); // 云崖

</script>

别名导入

  为了防止多个模块下接口名相同,我们可以使用as别名导入,再使用时也应该按照别名进行使用。

  下面是m1模块导出的代码:

let module_name = "js_m1";

let show = function () {
        console.log("js_m1.show");
}

export { module_name, show };

  下面是m2模块导出的代码:

let module_name = "js_m2";

let show = function () {
        console.log("js_m2.show");
}

export { module_name, show };

  下面是使用别名导入这两个模块的接口并进行使用:

<script type="module">

    import { module_name as m1_name, show as m1_show } from "./js_m1.js";
    import { module_name as m2_name, show as m2_show } from "./js_m2.js";

    console.log(m1_name);  // js_m1
    console.log(m2_name);  // js_m2

    m1_show();  // js_m1.show
    m2_show();  // js_m2.show

</script>

动态导入

  使用importfrom的导入方式属于静态导入,必须将导入语句放在最顶层,如果不是则抛出异常。

  这是模块中的导出接口:

export function test() {
        console.log("测试功能");
}

  如果我们想在某种特定条件下才导入并调用改接口,使用importfrom的方式会抛出异常。

<script type="module">

    if (true) {

        import { test } from "./js_m3.js"; // Error

        test();  // 想在特定条件下执行模块中的测试功能
    }

</script>

  这个时候就需要用到动态导入,使用 import() 函数可以动态导入,实现按需加载,它返回一个 promise 对象。

<script type="module">

    if (true) {

        let m3 = import("./js_m3.js");

        m3.then((module)=> module.test()); // 测试功能

    }

</script>

  我们可以使用解构语法来将模块中的接口一个一个全部拿出来。

<script type="module">

    if (true) {

        let m3 = import("./js_m3.js");

        m3.then(({ test, }) => test()); // 拿出test接口

    }

</script>

合并使用

  如果有多个模块都需要被使用,我们可以先定义一个Js文件将这些需要用到的模块中的接口做一个合并,然后再将该文件导出即可。

  合并导出请将exportfrom结合使用。

// js_m1

export default class{  // 默认导出
        static register(){
                console.log("注册功能");
        }
}
// js_m2

export class Login{
        static login(){
                console.log("登录功能");
        }
}

export function test(){
        console.log("js_m2测试功能");
}
// index.js

// 合并导出

import js_m1 from "./js_m1.js";

// js_m1中有接口是默认导出,因此我们需要不同的导出方式 , 注意这里就导出了一个接口,即js_m1的注册类
export {default as js_m1_register} from "./js_m1.js";

// 导出js_m2中的接口,共导出两个接口。登录类和测试函数。
export * as js_m2 from "./js_m2.js";

  导入与使用:

<script type="module">

    import * as index from "./index.js";

    index.js_m1_register.register();  // 注册功能

    index.js_m2.Login.login();  // 登录功能

    index.js_m2.test();  //  js_m2测试功能

</script>

指令总结

表达式说明
export function show(){} 导出函数
export const name="Yunya" 导出变量
export class User{} 导出类
export default show 默认导出

const name = "Yunya"

export {name}

导出已经存在变量
export {name as m1_name} 别名导出
import m1_default from './m1_js.js' 导入默认导出
import {name,show} from '/m1_js.js' 导入命名导出
Import {name as m1_name,show} from 'm1_js.js' 别名导入
Import * as m1 from '/m1_js.js' 导入全部接口

编译打包

  由于module语法是Es6推出的,所以对老旧的浏览器兼容不太友好,这个时候就需要用到打包工具进行打包处理使其能让老旧的浏览器上进行兼容。

  首先登录 https://nodejs.org/en/ 官网下载安装Node.js,我们将使用其他的npm命令,npm用来安装第三方类库。

  在命令行输入 node -v 显示版本信息表示安装成功。

安装配置

  cd到你的项目路径,并使用以下命令生成配置文件 package.json

npm init -y

  修改package.json添加打包命令

...
"main": "index.js",
"scripts": {
    "dev": "webpack --mode development --watch"  // 添加这一句
},
...

  安装webpack工具包,如果安装慢可以使用淘宝 cnpm 命令

npm i webpack webpack-cli --save-dev

目录结构

index.html

--dist #压缩打包后的文件

--src

----index.js #合并入口

----style.js //模块

  index.html内容如下

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

  index.js内容如下

import style from "./style";
new style().init();

  style.js

export default class User {
  constructor() {}
  init() {
    document.body.style.backgroundColor = "green";
  }
}

执行打包

  运行以下命令将生成打包文件到 dist目录,因为在命令中添加了 --watch参数,所以源文件编辑后自动生成打包文件。

npm run dev
posted @ 2020-08-07 16:13  云崖先生  阅读(2333)  评论(1编辑  收藏  举报