模块化开发

作者: amanohina | 来源:发表于2021-01-31 15:49 被阅读0次

    模块化开发是当下最重要的前端开发范式之一

    模块化演变过程

    • Stage1 文件划分方式
      具体的做法就是每个功能及其相关状态数据各自单独放到不同的文件中,约定每个文件就是一个独立的模块,使用某个模块就是将这个模块引入到页面中,然后直接调用模块中的成员(变量/函数)
      缺点也就十分明显了:

      • 污染了全局作用域
      • 命名冲突问题
      • 无法管理模块依赖关系



    • Stage2 命名空间方式
      每个模块只暴露一个全局对象,所有的模块成员都挂载到这个对象中,具体的做法就是在第一阶段基础之上,通过将每个模块包裹为一个全局对象的形式实现,有点类似于为模块内的成员添加了命名空间的感觉
      通过【命名空间】这一概念减少了命名冲突的可能,但是同样的,没有私有空间,所有的模块成员都可以在模块外部被访问或者是被修改,而且没有办法管理模块之间的依赖关系


    • Stage3 IIFE 立即执行函数表达式
      将每个模块成员都放在一个函数提供的私有作用域中,对于需要暴露给外部的成员,通过挂在到全局对象上的方式来实现,有了私有成员的概念,私有成员只能在模块成员内部通过闭包的形式访问


      需要暴露给外部的成员就使用这种挂载到全局作用域上面去实现
    • Stage4 模块化演变
      利用IIFE参数作用依赖声明使用,具体做法就是在第三阶段的基础上,利用立即执行函数的参数传递模块依赖项,使得每一个模块之间的关系变得更加明显
      例如使用jQuery,就使用立即调用函数接受jQuery的参数
      以上就是早期在没有工具和规范的情况下,对模块化的落地方式

    模块化规范的出现

    需要的内容就是:
    模块化标准+模块加载器

    CommonJS规范(node.js中的规范)

    • 一个文件就是一个模块
    • 每个模块都有单独的作用域
    • 通过module.exports导出成员
    • 通过require函数载入模块

    CommonJS是以同步模式加载模块
    在浏览器中必然会导致效率低下

    AMD(Asynchronous Module Definition)

    异步模块定义规范

    require.js

    require.js实现了AMD规范,本身也是很强大的模块加载器

    require.js定义一个模块,第一个参数就是模块的名字,第二个参数是数组,声明模块的依赖项,第三个参数是函数,函数内参数与依赖项一一对应,每一项是依赖项导出的成员,函数的作用是为当前模块提供一个私有的空间,如果需要向外部导出一些成员,可以通过return实现
    自动加载一个模块,只是用来加载模块,其他参数作用与define类似
    目前绝大多数第三方库都支持AMD规范
    • AMD使用起来相对复杂
    • 模块JS文件请求频繁,效率低下

    Sea.js+CMD

    这些以前的知识在目前来看也是很重要的一环

    模块化标准规范(模块化的最佳实践)

    • 在node环境当中,会采用CommonJS规范
    • 在浏览器环境中,会采用一个叫做ES Modules规范

      现如今绝大多数浏览器都已经支持ES Modules,故而ES Modules的学习成为了重中之重

    ES Modules

    • 通过script 添加type = module 的属性,就可以以ES Module的标砖执行其中的JS代码
        <script type="module">
            console.log("this is ES modules")
        </script>
    
    • ESM 会自动采用严格模式,忽略use strict
      (在非严格模式下,this指向的是window对象)
        <script type="module">
            console.log(this)
        </script>
    
    • 每个ES Module 都是运行在单独的私有作用域当中(第二个打印的foo就会报错undefined)
    
        <script type="module">
            var foo = 100
            console.log(foo)
        </script>
    
        <script type="module">
            console.log(foo)
        </script>
    
    • 在ESM中是通过CORS的方式请求外部JS模块的
    • ESM 的script标签会延迟执行脚本
      (延迟加载脚本,先渲染元素到页面上,一般的script标签就会等到脚本加载完成才会渲染元素)
      这个小特点与script标签的defer属性是一样的
        <script type= "module" src="demo.js"></script>
        <p>需要显示的内容</p>
    

    ES Modules导入和导出

    • 可以导出变量,函数,类等等
    export var name = 'foo module'
    
    export function hello(){
        console.log("foo hello")
    }
    
    export class Person{
    
    }
    
    • 也可以统一导出,比如:
    export { name , hello , Person}
    
    • 在另一个模块js文件要导入
    import { hello, name } from './module.js'
    console.log(name)
    hello()
    

    重命名

     var name = 'foo module'
    
     function hello(){
        console.log("foo hello")
    }
    
     class Person{
    
    }
    
    export { 
        name as fooName,
        hello as fooHello,
        Person as fooPerson
    }
    

    重命名之后导入时也要注意名字变化

    import { fooHello, fooName } from './module.js'
    console.log(fooName)
    fooHello()
    

    重命名特殊情况

    将导出成员名称设置为default,这个成员就会被设置为当前模块的默认导出成员,在导入的时候就必须要进行重命名

    export { 
        name as default,
        hello as fooHello,
        Person as fooPerson
    }
    

    重命名default才能调用

    import { fooHello, default as fooName } from './module.js'
    

    ESM 关于针对default的特殊处理

    将name变量设置为默认导出

    export default name;
    

    在导入的时候可以通过直接import + 变量名的方式接受默认导出的成员,变量名称随意

    // fooName这里是可以随意取名的
    import fooName from './module.js'
    

    ESM 导入导出的注意事项

    • export 后面跟上的花括弧包裹的不是字面量,是固定语法
    • 导入时的那些成员是分享的内存空间,是完全相同的引用关系
    • 导入的成员是只读的

    ESM import用法

    • 导入时from关键字后面跟的是字符串,内部的内容路径必须要完整的文件名称,不能省略js后缀名,跟CommonJS完全相反
    • 也可以使用完整的url加载模块,也就是说可以使用CDN上面的模块,完整的
    • 如果说只执行某个模块的功能,不去提取模块中的成员的话,可以保持花括弧为空,或者直接import跟上字符串,这个特性在我们导入一些不需要外界控制的子功能模块时就非常有用了
    import {} from './module.js'
    import './module.js'
    
    • 需要导出的成员特别多,导入时都会用到他们,就可以用*全部提取出来,使用as关键字全部存在对象里面
    import * as mod from './module.js'
    console.log(mod)
    
    • 动态导入
    import('./module.js').then(function (module) {
      console.log(module)
    })
    
    • 默认成员和明明成员同时导出
    var name = 'jack'
    var age = 18
    
    export { name, age }
    
    console.log('module action')
    
    export default 'default export'
    
    
    import abc, { name, age } from './module.js'
    console.log(name, age, abc)
    

    ESM 直接导出导入成员

    • 具体的做法就是将import关键词修改为export,所有的导入成员会作为当前模块的导出成员,在当前作用域下,也就不再可以访问这些成员了。
      一般用于index文件,把散落的模块通过这种方式组织到一起,导出,方便外部使用

    avatar.js:

    export var Avatar = 'Avatar Component'
    

    button.js:

    var Button = 'Button Component'
    export default Button
    

    index.js:

    export { default as Button } from './button.js'
    export { Avatar } from './avatar.js'
    

    app.js(导入):

    import { Button, Avatar } from './components/index.js'
    
    console.log(Button)
    console.log(Avatar)
    
    

    avatar和button都是暴露了组件,index.js则是将这两个组件导入,并且导出,作为一个桥梁的作用

    ESM in Browser(Ployfill兼容方案)

    • 让浏览器支持ESM 的绝大特性
    • 模块名字为Browser ESM Loader

    https://github.com/ModuleLoader/browser-es-module-loader

    针对NPM下的模块可以通过upkg这个网站的CDN服务来拿到所有的JS文件

    https://unpkg.com/ + npm下的模块名

    比如
    https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js

    /dist/表示目录下的文件

    将对应的路径复制下浏览器地址用script标签引入就可
    • 引入IE所需要的promise,ployfill
     <script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
      <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
      <script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
    
    • nomodule属性
      解决了支持polyfill的浏览器不去加载标签内资源的问题

    ESM in Node.js

    • 在Node当中直接使用ESM ,要做的有:
      • 第一,将文件的扩展名由 .js 改为 .mjs;


      • 第二,启动时需要额外添加 --experimental-modules 参数;

    • 也可以用ESM 载入原生模块
    // // 此时我们也可以通过 esm 加载内置模块了
    import fs from 'fs'
    fs.writeFileSync('./foo.txt', 'es module working')
    
    • 也可以直接提取模块内的成员,内置模块兼容了ESM的提取成员的方式
    import { writeFileSync } from 'fs'
    writeFileSync('./bar.txt', 'es module working')
    
    • 对于第三方的NPM模块也可以通过ESM加载
      (比如第三方模块lodash)
    import _ from 'lodash'
    _.camelCase('ES Module')
    
    • 但是不能使用ESM的花括弧方式去载入第三方模块的成员
    // // 不支持,因为第三方模块都是导出默认成员
    import { camelCase } from 'lodash'
    console.log(camelCase('ES Module'))
    

    ESM in Node.js 与 CommonJS模块的交互

    • CommonJS模块始终只会导出一个默认成员
    • ESM 中是可以导入CommonJS模块的
    • 不能直接提取成员,import不是解构导出对象
    • 在CommonJS中通过require载入ESM 也是不可以的


    ESM in Node.js与CommonJS的差异

    在这之前先推荐使用nodemon工具,可以监听mjs文件的变化并且给出错误信息
    先用npm 进行全局安装,再使用

    • ESM中没有模块全局成员了
    • require,module,exports自然是可以通过import和export代替
    • __filename 和 __dirname 通过 import 对象的 meta 属性获取
    const currentUrl = import.meta.url
    console.log(currentUrl)
    
    • 通过 url 模块的 fileURLToPath 方法转换为路径
    import { fileURLToPath } from 'url'
    import { dirname } from 'path'
    const __filename = fileURLToPath(import.meta.url)
    const __dirname = dirname(__filename)
    console.log(__filename)
    console.log(__dirname)
    

    Node的新版本更加支持ESM了

    • 在新版本中的package.json添加type属性表示module,所有的JS文件就可以默认以ESM支持了
    • 如果需要在 type=module 的情况下继续使用 CommonJS, 需要将文件扩展名修改为 .cjs

    Babel兼容方案

    • 早期的node版本,可以使用Babel进行ESM的兼容
    • 主流的JavaScript编译器,可以将新特性的代码编译成当前环境支持的代码
      需要安装babel一系列依赖
    yarn add @babel/node @babel/core @babel/core @babel/preset-env --dev
    
    • 检测babel命令:


    • 安装插件
    yarn add @babel/plugin-transform-commonjs --dev
    
    • 建立一个.babelrc文件
    {
      "plugins": [
        "@babel/plugin-transform-modules-commonjs"
      ]
    }
    
    
    • 运行文件
     yarn babel-node .\index.js
    

    相关文章

      网友评论

        本文标题:模块化开发

        本文链接:https://www.haomeiwen.com/subject/jznrzktx.html