模块化

从本文你将理解到

  • 什么是模块化
  • 模块化的进化史
  • 当下罕用的模块化标准 CommonJS,ES Module
  • ES Module个性
  • ES Module应用in Browsers , Node.js
  • ES Modules in Node.js - 与 CommonJS 交互
  • ES Modules in Node.js - 与 CommonJS 差别

模块化

  • 前端开发范式 是一种思维
  • 依据性能不同将代码划分不同模块,从而进步开发效率,升高保护老本

模块化的进化史

stage-1文件划分形式

<body>  <h1>模块化演变(第一阶段)</h1>  <h2>基于文件的划分模块的形式</h2>  <p>    具体做法就是将每个性能及其相干状态数据各自独自放到不同的文件中,    约定每个文件就是一个独立的模块,    应用某个模块就是将这个模块引入到页面中,而后间接调用模块中的成员(变量 / 函数)  </p>  <p>    毛病非常显著:    所有模块都间接在全局工作,没有公有空间,所有成员都能够在模块内部被拜访或者批改,    而且模块一段多了过后,容易产生命名抵触,    另外无奈治理模块与模块之间的依赖关系  </p>  <script src="module-a.js"></script>  <script src="module-b.js"></script>  <script>    // 命名抵触    method1()    // 模块成员能够被批改    name = 'foo'  </script></body>
module-a.jsvar name = 'module-a'function method1 () {  console.log(name + '#method1')}function method2 () {  console.log(name + '#method2')}

stage-2命名空间形式

<body>  <h1>模块化演变(第二阶段)</h1>  <h2>每个模块只裸露一个全局对象,所有模块成员都挂载到这个对象中</h2>  <p>    具体做法就是在第一阶段的根底上,通过将每个模块「包裹」为一个全局对象的模式实现,    有点相似于为模块内的成员增加了「命名空间」的感觉。  </p>  <p>    通过「命名空间」减小了命名抵触的可能,    然而同样没有公有空间,所有模块成员也能够在模块内部被拜访或者批改,    而且也无奈治理模块之间的依赖关系。  </p>  <script src="module-a.js"></script>  <script src="module-b.js"></script>  <script>    moduleA.method1()    moduleB.method1()    // 模块成员能够被批改    moduleA.name = 'foo'  </script></body>****
module-a.jsvar moduleA = {  name: 'module-a',  method1: function () {    console.log(this.name + '#method1')  },  method2: function () {    console.log(this.name + '#method2')  }}

stage-3 IIFE形式

<body>  <h1>模块化演变(第三阶段)</h1>  <h2>应用立刻执行函数表达式(IIFE:Immediately-Invoked Function Expression)为模块提供公有空间</h2>  <p>    具体做法就是将每个模块成员都放在一个函数提供的公有作用域中,    对于须要裸露给内部的成员,通过挂在到全局对象上的形式实现  </p>  <p>    有了公有成员的概念,公有成员只能在模块成员内通过闭包的模式拜访。    能够通过IIFE向模块外部传参  </p>  <script src="module-a.js"></script>  <script src="module-b.js"></script>  <script>    moduleA.method1()    moduleB.method1()    // 模块公有成员无法访问    console.log(moduleA.name) // => undefined  </script></body>
module-a.js;(function () {  var name = 'module-a'    function method1 () {    console.log(name + '#method1')  }    function method2 () {    console.log(name + '#method2')  }  window.moduleA = {    method1: method1,    method2: method2  }})()

stage-4 AMD标准

<body>  <h1>模块化标准的呈现</h1>  <h2>Require.js 提供了 AMD 模块化标准,以及一个自动化模块加载器</h2>  <script src="lib/require.js" data-main="main"></script></body>
module1.js// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块// 所以应用时必须通过 'jquery' 这个名称获取这个模块// 然而 jQuery.js 并不在同级目录下,所以须要指定门路// define API 参数:模块名,依赖项(可选参数),函数(函数参数与依赖项对应)为以后模块提供公有空间define('module1', ['jquery', './module2'], function ($, module2) {  return {  // 公有空间向外导出成员通过return    start: function () {      $('body').animate({ margin: '200px' })      module2()    }  }})
module2.js// 兼容 CMD 标准(相似 CommonJS 标准)define(function (require, exports, module) {    // 通过 require 引入依赖  var $ = require('jquery')  // 通过 exports 或者 module.exports 对外裸露成员  module.exports = function () {    console.log('module 2~')    $('body').append('<p>module2</p>')  }})

以后模块化局势

模块化标准 CommonJS,ES Module

  • CommonJS标准

    • nodejs提出的一套规范
    • nodejs代码必须遵循CommonJS标准
    • 一个文件就是一个模块
    • 每个模块都有独自的作用域
    • 通过module.exports导出成员
    • 通过require函数载入模块
  • CommonJS是以同步模式加载模块
  • node端: node的执行机制是在启动时加载模块。执行过程中不须要加载,只会应用到模块,因而node环境下应用commonjs没有问题
  • 浏览器端: 必然导致效率低下,每次页面加载都会导致大量同步模式申请呈现,因而晚期前端模块化中并没有应用commonjs标准,而是依据浏览器特点设计了专门用于浏览器端的标准AMD(Asynchronous Module Definition),和库Require.js(实现了AMD标准),Require也是个十分弱小的模块加载器
  • 目前绝大多数第三方库都反对AMD标准,但应用起来绝对简单,另外如果我的项目模块划分过细,那么同一个页面对js文件的申请次数就会特地多,从而导致页面效率低下
  • 同期呈现了淘宝Sea.js库+CMD标准,当然起初也被Require.js兼容了

ESModule个性

  • 装置 yarn global add serve
  • 执行 serve ./src/xxx 查看文件
  • 给script 增加type=module 属性,就能够以 ESModule 的规范执行其中的 JS代码

    <script type="module">console.log('this is es module');</script>

    相比于一般script标签,esm的特点

  • 主动采纳严格模式,疏忽 'use strict'

    <script type="module">console.log(this); //undefined</script>
  • 每个 ES Module 都是运行在独自的公有作用域中

    <script type="module">var foo = 100;console.log(foo);</script><script type="module">console.log(foo); //报错</script>
  • ESM 是通过 CORS 的形式申请内部 js模块的

意味着如果申请的js不在同源目录下。申请的服务端地址,它在响应的响应头中须要提供无效的CORS标头,也就是申请地址须要反对CORS

//申请胜利<script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>//申请失败<script type="module" src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>  
  1. ESM 的script标签会提早执行脚本 等同于defer属性

默认按执行程序,script立刻执行,页面的渲染会期待脚本执行后再持续渲染

增加type后,执行程序并不等于引入程序

<script type="module">  alert("Hello")</script><p>须要显示的内容</p>

ESM 导入和导出

每个模块领有公有作用域,因而内部无法访问

通过export ,import进行模块裸露和载入

export,import

  • export能够导出变量,函数,类

    export var foo = {}export var fn = function(){}export class Person{}
  • 独自应用export,更直观形容导出成员

    var foo = {}var fn = function(){}export {foo,fn}
  • 导出重命名

    var foo = {}var fn = function(){}export {foo as baz,fn} //引入时通过baz引入
  • 重命名为default,默认导出

    var foo = {}var fn = function(){}export {foo as default,fn} //默认导出必须重命名 import {default as baz} from ''
  • 默认导出写法2

    export default {foo, fn} //接管对象成员 import mod from '' | mod.fn应用

    导出注意事项

  • export {}不是字面量对象,import引入的也不是解构,都是固定语法,因而 export foo 也是不被容许的,要应用export var foo,而export default {} 默认导出,导出的是字面量对象

    # a.jsvar foo = {}var fn = function(){}export {foo,fn}...# b.jsimport {foo,fn} from ''
  • 通过export导出的不是值,而是值的地址,内部取值会受到外部值批改的影响

    # a.jsexport {foo,fn}setTimeout(function(){foo="ben"},1000)...# b.jsimport {foo,fn} from ''console.log(foo)setTimeout(function(){console.log(foo)},1500)
  • 内部导入的成员属于只读成员(常量),无奈批改

    #b.jsimport {foo,fn} from ''console.log(foo)foo = "bau" //报错

    导入注意事项

  • import xx from './module.js' 门路名称必须残缺不能省略,省略会报错
  • import xx from '/xx/module.js' 导入外部文件应用相对路径或者绝对路径,'./'不能省略,否则会认为是加载模块
  • import xx from 'http://localhost:3000/xx/module.js' 能够应用残缺的url拜访
  • import {} from './module.js' 只会执行模块,不会提取成员
  • import './module.js' 下面能够简写成 import '模块门路',导入一些不须要管制的子功能模块十分好用
  • import * as mod from './module.js' 当导出内容很多,用* as mod 提取进去,通过mod.xx应用成员
  • import() 动静导入模块
应用动静导入模块解决//无奈用变量var modulePath = "./module.js"import { name } from modulePath;//无奈条件判断,只能放在最顶层作用域if(true){  import {name} from './module.js'}//全局的import函数,用来动静导入模块,返回promiseimport('./module.js').then(function(module){  console.log(module); //导入的模块对象通过参数拿到})
  1. import title,{foo,fn} from './module.js' 对于导出的默认成员能够将其提取进去,也能够写成 import {foo,fn, default as title} from './module.js'

导入导出成员

export {name, age} from '' 所有导入成员作为以后模块的导出成员,那么导入的成员无法访问

# index.js 临时文件import { Button } from './xxx'import { Avatar } from './xxx1'export { Button,Avatar }//改写成export { Button } from './xxx'export { Avatar } from './xxx1'

ESM in Browsers

ie下用Polyfill,增加CDN减速,解决esm用不了的问题

Polyfill 是一块代码(通常是 Web 上的 JavaScript),用来为旧浏览器提供它没有原生反对的较新的性能,能够了解为是一种兼容计划,参考地址

配置运行环境
装置yarn global add browser-sync

应用browser-sync . --files **/*.js

或者vscode 装置个live Server启用

// 通过unpkg网站提供的cdn服务拿到js文件// https://unpkg.com/browse/browser-es-module-loader@0.4.1/dist/ // https://unpkg.com/browse/promise-polyfill@8.2.0/dist/
<body>  <!-- nomodule布尔值 不反对模块化的浏览器会执行,不然chrome执行两次,只适宜开发阶段 -->  <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>  <script type="module">    import { foo } from './module.js'    console.log(foo);  </script></body>

ESM in Node.js

nodejs中应用ESM须要批改文件名从 .js => .mjs

node启动文件应用 node --experimental-modules xxx.mjs

案例

# module.mjsexport const foo = 'hello'export const bar = 'world'
# index.mjsimport { foo, bar } from "./module.mjs";console.log(foo,bar);//node 中应用esm 须要批改文件名为 .mjs/** * node --experimental-modules index.mjs    (node:9696) ExperimentalWarning: The ESM module loader is experimental.    hello world * *///通过ESM载入原生模块import fs from "fs";fs.writeFileSync('./foo.txt','es module working')//零碎内置模块,能够通过这种形式导入,内置模块兼容了ESM的提取成员形式import { writeFileSync } from "fs";writeFileSync('./bar.txt','es module working~ bar')//通过ESM载入第三方模块import _ from "lodash" //须要yarn add loadshconsole.log(_.camelCase('ES MODULE'))//不反对,因为第三方模块都是导出默认成员,只能用默认导入// import { camelCase } from "lodash";// console.log(camelCase('ES Module'));

ESM 与 CommonJS交互

# commonjs.js//CommonJS模块始终只会导出一个默认成员//这也就意味着只能通过import载入默认成员形式引入module.exports = {  foo: 'commonjs exports value foo'}exports.baz = 'commonjs exports value baz'console.log("---------------------------------------");// node --experimental-modules commonjs.js//不能在 CommonJS模块中通过require 载入 ESMconst mod = require('./es-module.mjs')console.log(mod);//报错
# es-module.mjs// node --experimental-modules es-module.mjs//ESM中能够导入 Commonjs模块import mod from "./commonjs.js";console.log(mod); //ok//不能间接提取成员,留神import不是解构导出对象import { baz } from "./commonjs.js";console.log(baz); //报错import baz from './commonjs.js' //okconsole.log("---------------------------------------------");export const foo = 'es module export value'

总结

  • ES Modules中能够导入CommonJS模块
  • CommonJS中不能导入ES Modules模块
  • CommonJS始终只会导出一个默认成员
  • 留神import 不是解构导出对象

ESM 与 CommonJS差别

运行环境nodemon --experimental-modules xxx.mjs

在commonjs中的对象在ESM中会报错,解决办法是用其余办法代替,报错办法有

# cjs.js//加载模块函数console.log(require);//模块对象console.log(module);//导出对象别名console.log(exports);//以后文件的绝对路径console.log(__filename);//以后文件所在目录console.log(__dirname);
# esm.mjs// 前三个能够应用export import代替// 后两个import { fileURLToPath } from "url";const __filename = fileURLToPath(import.meta.url)console.log(__filename); //d:\Users\admin\Desktop\part2-2\differences\esm.mjsimport { dirname } from 'path'const __dirname = dirname(__filename)console.log(__dirname); //d:\Users\admin\Desktop\part2-2\differences

ESM in Nodejs 进一步反对

将package.json中增加type = 'module',这样不必批改.js为.mjs

# package.json{  "type":"module"}// 运行命令`nodemon --experimental-modules xxx.js`

如果增加了type的我的项目中还想应用CommonJS

# common.jsconst path = require('path')console.log(path.join(__dirname,'foo'))

会报错,解决办法将commonjs文件批改为 common.cjs

ESM in Nodejs Babel兼容计划

晚期node版本8.0.0,通过babel来让node运行esm

装置babel-mode yarn add @babel/node @babel/core @babel/preset-env --dev

babel是基于插件机制去实现的,外围core和preset-env并不会转换代码,具体转换个性通过插件

一个插件转化一个个性,preset-env是插件汇合,这个汇合蕴含了js规范中所有新个性

理论帮咱们转换的是汇合里的插件

能够通过yarn babel-node index.js --presets=@babel/preset-env

或者放入配置文件.babelrc json格式文件

{  "presets":["@babel/preset-env"]}

执行yarn babel-node index.js

或者移除汇合应用插件

通过 yarn remove @babel/preset-env

yarn add @babel/plugin-transform-modules-commonjs --dev

{  "plugins":[    "@babel/plugin-tranform-modules-commonjs"  ]}