模块化开发

模块化开发


1. 模块化开发概述

模块化开发目前是当下最重要的前端开发范式之一,模块化并不是一种技术,而是一种开发思想。
模块化将我们的复杂的代码根据功能划分为不同的模块单独去维护,通过这种方式提升开发效率和降低维护成本。

2. 模块化的演变过程

早期的前端技术标准根本没有意识到前端应用能有今天这样的规模,所以很多设计上的遗留问题就导致今天去实现前端模块化会遇到很多问题。当然了,如今都被一些标准或者工具帮助我们解决了这些问题,但是它的演进过程是值得我们去思考的。

  • stage1-文件划分
    最早期的前端模块化是根据文件划分实现的,就是将每个功能的代码单独存放在不同的文件当中,我们约定每个文件就是独立的模块,然后在开发文件引入它们,调用模块的全局成员。但是显然这种方式的缺点是十分明显的,模块内所有的成员都可以在模块外被访问使用或修改,模块一旦多了的话,就难免产生命名冲突,而且我们也无法管理模块之前的依赖关系。总的来说,这种方式全部依靠人为的约定,一旦项目上了体量,这种方式的缺陷就是致命的。

  • stage2-命名空间方式
    文件划分的方式缺陷太多了,所以就有了第二个阶段的模块化,那就是在模块内暴露一个全局的对象,模块内的所有数据都挂载在这个全局对象中。但是这种方式虽然减少了命名冲突的可能,但是仍然没有私有空间,还是没有解决 stage1 的根本问题

  • stage3-IIFE(Immediately-Invoked Function Expression)
    为了解决变量私有化的问题,有些人想到了可以在模块内定义个立即执行函数,私有成员都放在这个立即执行函数中,然后需要暴露出去的成员,我们挂载到一个全局对象中去,这样就实现了私有成员的概念。私有成员我们只能通过模块内闭包的方式去访问,而在模块外部是没有办法去使用的。而且我们还可以通过这个立即执行函数的参数去作为我们的依赖声明去使用,这样就使得我们每个模块的依赖声明显得更加明显了。

以上这些就是早期在没有模块化工具和规范的情况下,我们去实现前端模块化的方式,虽然这些方式帮助我们解决了模块化各种各样的问题,但是它仍然存在一些没有解决的问题。

3. 模块化规范的出现

为了统一不同前端开发者和不同项目的差异,我们就需要一个标准去规范我们实现前端模块化的方式。

  • CommonJS 规范
    了解过模块化的人,肯定知道 CommonJS 规范,它是 Node 实现的的前端模块化规范,它有如下几个约定

    • 一个文件就是一个模块
    • 每个模块都有单独的作用域
    • 通过 module.exports 导出成员
    • 通过 require 函数载入模块
      CommonJS 是以同步方式去加载模块,因为 Node 是在启动时去加载模块,在执行过程中是不需要加载模块的。但是这种方式在浏览器中去使用就会出现问题,如果我们浏览器每次加载页面都有大量的同步模块去加载的话,就会导致浏览器执行的效率特别低下。
  • AMD(Asynchromous Module Definition)规范
    AMD 规范是通过 require.js 实现的,require.js 本身是一个强大的模块加载器。每个模块都需要通过 define 这个函数去定义,这个函数有三个参数,第一个参数是模块的名称;第二个参数是一个数组,用来声明模块的依赖项;第三个参数是一个函数,这个函数的参数与第二个参数的依赖项一一对应,这个函数的作用就是为当前这个模块去提供一个私有空间。

除此之外,require.js 还帮助我们去实现了一个 require 的函数,这个函数帮助我们去载入一个模块。

那么一旦 require 去加载一个模块的时候,他就会去创建一个 script 标签,然后去请求这个脚本文件,并且执行相应模块的代码。目前绝大多数的第三方库都支持 AMD 规范,但是它也有很多缺点:
* AMD 使用起来相对复杂
* 模块 js 文件请求复杂
AMD 只能算是前端模块化演进道路上的一步,它是一个妥协的实现方式,并不能算是最终的解决方案,但是在当时的环境背景下,它还是很有意义的,因为毕竟它为前端模块化提供了一种标准。

  • CMD(Common Module Definition)规范
    CMD 规范是由淘宝的前端团队推出的,它是通过 sea.js 去实现的。它的实现有点类似 CommonJS,在使用上和 require.js 差不多

开始这样设计是为了减少学习成本,后来这种方式也被 require.js 兼容了。

现在前端的模块化标准已经非常清晰了,那就是在 Node 环境中我们遵循 CommonJS 规范,在浏览器中环境中我们会采用一个叫做 ES Modules 的规范。ES Modules 是在 ES6 中实现的模块化标准,所以它会存在各种各样的环境兼容问题,随着 Webpack 等打包工具的普及,这一规范才开始普及。现在 ES Modules 基本已经是前端最主流的模块化规范了,相比于 CommonJS 这种社区规范,ES Modules 可以说是真正在语言层面上实现了模块化。

4. ES Modules 常见特性

目前市面上绝大多数的浏览器都支持 ES Modules,我们想要使用它的特性,只需要在 html 文件中,给 script 标签添加一个 type = module 的属性,就可以用 ES Modules 的标准去执行其中的代码了。下面我们来看看 ES Modules 都有什么特性。

    1. 需要注意的是,在 ES Modules 规范中,自动采用严格模式,不用你手动添加 'use strict',严格模式有一个代表,就是不能在全局环境中使用 this
<script type="module">
    console.log(this) // undefined
</script>
    1. 每个 ES Module 都是运行在单独的私有作用域中
<script type="module">
    var foo = 'bar'
    console.log(foo) // bar
</script>

<script type="module">
    console.log(foo) // Uncaught ReferenceError: foo is not defined
</script>

可以看到在 ES Module 规范下,我们在一个 script 中定义变量后,再在另一个 script 中是取不到这个变量的,说明每个 module 都是一个私有作用域,这样我们就不用担心变量污染的问题了。

    1. ES Module 是通过 CORS 的方式请求外部 JS 模块的
      在 ES Module 规范下,请求服务器端地址时,是通过 CORS 的方式去请求的,如果当前地址不支持 CORS,就会产生跨域,下面的地址就是不支持 CORS的
<script type="module" src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js">
</script>

我们来看一下浏览器的控制台

可以看到是报了一个跨域的错误的,下面我们来更换一个支持 CORS 的地址

<script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js">
</script>

我们再来看一下控制台

这回我们可以看到请求结果是正常的了

    1. ES Module 的 script 标签会延迟执行脚本
      正常如果我们在 html 之前引入的 script 标签,如果 js 脚本有阻塞性的代码,那么就会影响 html 的渲染。例如我们创建一个 demo.js
// demo.js
alert('这是一个弹窗')

我们在另一个 html 中引入这个 demo.js 文件

<script src="demo.js"></script>
<p>需要显示的内容</p>

那么它就会先弹出一个弹窗,等我们点击确定之后,才会显示 p 标签内的内容。但是当我们使用 ES Module 规范时,它就会延迟执行脚本,等待网页渲染完成后再去执行脚本文件,就相当于使用了 defer 属性

<script type="module" src="demo.js"></script>
<p>需要显示的内容</p>

// 等价于
<script defer src="demo.js"></script>
<p>需要显示的内容</p>

5. ES Modules 导入导出

使用 ES Modules 就可以使用 import 和 export 关键字

  • export
    (1)直接导出
export var name = 'this is module'

export function hello() {}

export class Person {}

(2)导出成员

var name = 'this is module'

function hello() {}

class Person {}

export { name, hello, Person }

这种方式更为常见一些,我们可以更加清晰看到各种导出的模块成员
(3)重命名导出

// a.js
var name = 'this is module'

function hello() {}

export {
    name as fooName,
    hello as fooHello,
}

// 导入
import fooName from 'a.js'
import fooHello from 'a.js'

(4)默认导出
如果我们到导出成员的时候,将成员重命名为 default 的话,那么就是默认导出成员

// a.js
var name = 'this is module'

export {
    name as default
}

// 导入
import { default as name } from 'a.js'

在 ES Modules 规范中,也支持我们直接默认导出成员,而且我们在导入的时候,可以将它命名为任意名称。

// a.js
var name = 'this is module'

export default name

// 导入
import anyName from 'a.js'

需要注意的是 export 导出成员的时候 export {} 是一个固定的语法,而不是去导出一个对象变量,在 import 的时候,也不是结构对象。
通过 export 导出的成员就是成员本身,所以说在模块内部修改了这个成员,模块外部导出的成员也会跟随改变的

// a.js
const name = 'jack'
export { name }

setTimeout(() => {
    name = 'ben'
}, 1000)

// b.js
import { name } from 'a.js'

console.log(name) // jack

setTimeout(() => {
    console.log(name) // ben
}, 1500)

而且 export 导出的成员是一个常量,不可以被修改

// a.js
export var name = 'jack'

// b.js
import { name } from 'a.js'
name = 'tom' // 报错
  • import
    (1)导入用法
    值得注意得是,在ES Modules 规范下,import 导入的路径的后缀名必须写完整,要不然就会无法识别,包括默认的 index,而且在我们去引入相对路径的时候,还必须将 "./" 写上,否则会当成一个包文件引入
// 完整路径
import { name } from './module.js'

当然了,这种缺陷可以通过像 webpack 这样的构建工具去解决
我们也可以通过文件的绝对路径去引入

import { name } from '/module/module.js'

我们也可以通过 url 的方式去引入文件,这也意味着我们可以去访问 cdn 上的文件

import { name } from 'http://localhost:300/04-import/module.js'

当我们只想去执行某个模块,而并不需要去提取这个模块中的成员的时候,我们就可以将 "{}" 中置为空就可以了

import {} from './module.js'
// 上面的写法可以简写为
import './module.js'

如果我们想导入这个模块的所以成员,我们可以使用 "*" 的形式

// mod 是一个对象,它的属性包含所有成员
import * as mod from './module.js'
console.log(mod)

如果我们想动态导入一个模块,我们可以将路径放在 import 的执行函数中,这个执行结果返回一个 promise,我们可以通过 then 那到它的成员

import('./module.js).then(function (module) {
    ...
})

如果在导出文件中同时导出了部分成员和默认成员

// 导出文件 module.js
var name = 'maoxiaoxing'
var age = '25'
export { name, age }
export default 'default export'

// 导入文件中
import { name, age, default as title } from './module.js'
console.log(name, age, title)

上面的导入默认成员的方式,还有简写的方式

import title, { name, age } from './module.js'
  • 导出导入成员
    如果我们想将导入的成员直接导出的,话我们只需要将 import 换成 export 即可
export { foo, bar } from './module.js'

6. ES Modules 环境兼容

ES Modules 规范是2014年才被提出来的,所以早期的浏览器是不可能支持这个特性的,那么我们就要考虑浏览器的兼容性问题了。

在 ie 和一些国产浏览器上,ES Modules 规范还没有被支持

  • 浏览器环境 Polyfill

要想在各个浏览器上兼容 ES Modules 规范,我们可以使用 browser-es-module-loader 来兼容我们的代码。我们可以直接在 https://unpkg.com/ 上面通过 script 标签引入这段代码。

// module.js
export var foo = 'bar'
// 兼容 promise
<script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
// babel 即时运行在浏览器
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
// browser-es-module-loader
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
<script type="module">
    import { foo } from './module.js'
    console.log(foo)
</script>
  • ES Modules in Node.js
    Node.js 在8.5版本过后,也开始逐步支持 ES Modules 规范了,但是考虑到 ES Modules 规范和 commonjs 规范的差距还是比较大的,所以目前这个特性还是处于一个过渡的状态。
    要想在 node 中使用 ES Modules 规范,第一件事我们需要将以 .js 结尾的文件名改为 .mjs
// module.mjs
export const foo = 'hello'
export const bar = 'world'
import { foo, bar } from './module.mjs'
console.log(foo, bar) // hello world

需要注意的是,在我们执行 node 文件时,我们需要 node XXX.js 修改为下面的命令

node --experimental-modules index.mjs

通过这样的方式来告知 node 我们是用 ES Modules 规范执行我们的代码。但是这只是一个实验特性,我们在生产环境不要这样做。

posted @ 2020-11-15 22:14  毛小星  阅读(398)  评论(0编辑  收藏  举报