模块化开发:CommonJs、AMD、ESM来啦!

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

模块化演变过程

stage1.文件划分方式

完全依靠约定

缺点

  • 没有私有的独立空间,在模块外部可以被随意的访问和修改,污染全局作用域
  • 模块多了之后会产生命名冲突问题
  • 无法管理模块之间的依赖关系

stage2.命名空间方式

  • 可以减少命名冲突的问题
  • 但是没有私有空间,依然可能被外部所修改
  • 模块之间的依赖关系没有被解决

stage3.IIFE(立即执行函数)

使用立即执行函数为模块提供私有空间

模块化规范

模块化标准+模块加载器

Commonjs规范(nodejs的规范标准)

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

CommonJS是以同步模式加载模块,启动时加载模块,执行的过程中是不需要加载的。在浏览器中会导致效率低下。启动时就需要加载大量的内容,所以浏览器没有直接使用commonJS规范

AMD(Asynchronous Moudle Definition)异步的模块定义规范

Require.js 模块加载器

目前绝大多数第三方库都支持AMD规范

  • AMD使用起来较为复杂
  • 模块JS请求较为频繁

淘宝推出Sea.js +CMD

模块化标准规范最佳实践

  • ES Modules-最主流的模块开发规范
  • CommonJS

ES Module的特性

  • 1.ESM自动采用严格模式忽略’use strict‘
  • 2.每个ESM都是作用在单独的私有作用域当中
  • 3.ESM是通过CORS的方式请求外部js模块的
  • 4.ESM的script标签会延迟脚本的执行
    相当于defer属性
<!-- 通过给script添加type = module属性,就可以以ES Module的标准执行其中的js代码了 -->
    <script type="module">
        console.log("this is a module")
    </script>
    <!-- 1.ESM自动采用严格模式忽略’use strict‘ -->
    <script type="module">
        /**
         *  严格模式中不能在全局范围直接使用this
         * 非严格模式下this指向的是全局对象
        */
        console.log(this)
    </script>
    <!-- 2.每个ESM都是作用在单独的私有作用域当中 -->
    <script type="module">
        var foo= '123'
        console.log(foo)
    </script>
    <script type="module">
        // console.log(foo)
    </script>
    <!-- 3. ESM是通过CORS的方式请求外部js模块的
        这就要求我们请求我们的模块不在同源地址的话,
        要求请求的js模块的响应头当中提供有效的cors标头

        cors不支持文件的形式访问,所以必须使用http server 的方式去让页面工作起来
     -->
     <script type="module" src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>

     <!-- 4. ESM的script标签会延迟脚本的执行 
    相当于defer属性
    -->
     <script type="module" src="demo.js"></script>
     <p>这段文字不会被上面的引入延迟</p>
复制代码

ESM的导入和导出

  • 使用

    npm install browser-sync -g
    //监视文件的更新,来自动更新界面的显示
    browser-sync . --files **/*.js
    复制代码
    /**
     * ESM每一个模块都会作用到独立的私有作用域当中
     * 模块内所有成员都没法在模块外被访问到
     */
    export var name = 'foo module'
    // 可以修饰函数
    export function hello() {}
    //可以修饰class
    export class Person{}
    复制代码
  • 还可以集中导出当前模块要暴露出的内容,这种方式更直观

    var name = 'foo module'
    function hello() { }
    class Person { }
    export {
        name, hello, Person
    }
    复制代码
  • 通过as关键词实现导出内容的重命名

    export {
    name as fooName, hello, Person
    }
    复制代码
  • 将导出成员重命名为default,那此成员就是当前模块的默认成员

    export {
        name as default, hello, Person
    }
    复制代码

    导入时就必须进行重命名

    //因为default为一个关键词,所以不能把它当做一个变量来使用所以就要使用as进行重命名
    import { default as fooName } from './module.js'
    console.log(fooName)
    复制代码
  • export default name为当前模块的默认导出方式

    导入的时候就可以根据自己的需要随便去取

    var name = 'foo module'
    export default name
    复制代码
    import someName from './module.js'
    复制代码

export的注意事项

  • 1.ES Module 中 { } 是固定语法,就是直接提取模块导出成员
  • 2.导入成员并不是复制一个副本,而是直接导入模块成员的引用地址,也就是说 import 得到的变量与 export 导入的变量在内存中是同一块空间。一旦模块中成员修改了,这里也会同时修改
  • 3.导入模块成员变量是只读的

import注意事项

  • 1.from后面必须是完整的文件路径不能省略.js扩展名,这个跟commonJS是有区别的

    import { name } from './module'
    import { name } from './module.js'
    console.log(name)
    复制代码
  • 2.ESM 也不能默认加载文件夹下的index.js文件,可以通过打包插件自行配置使用

    import { lowercase } from './utils'
    import { lowercase } from './utils/index.js'
    console.log(lowercase('HHH'))
    复制代码
  • 3.在网页资源中的相对路径的./是可以省略掉的,但是ESM中./不能省略,

    为省略后是以字母开头,默认以为是在加载第三方模块,这一点与commonJS相同

    也可以使用以/开头的绝对路径来进行访问

    也可以使用完整的URL来访问我们的模块,可以直接引用CDN上的一些文件

    import { name } from 'module.js'
    import { name } from './module.js'
    import { name } from '/04-import/module.js'
    import { name } from 'http://localhost:3000/04-import/module.js'
    console.log(name)
    复制代码
  • 4.如果只是执行摸个模块而不需要提取某个成员可以使用以下两种方式

    import {} from './module.js'
    import './module.js'
    复制代码
  • 5.如果导入的成员特别多,可以使用*来导入所有成员,然后放入mod中

    访问时直接使用mod.来访问即可

    import * as mod from './module.js'
    console.log(mod)
    复制代码

导出导入成员

/**
 * 在当前作用域中就不可以访问这些成员了
 */
export { foo, bar } from './module.js'
复制代码

ESM in Browser Polyfill兼容方案

只适合开发阶段的使用

<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>
复制代码

ESM in Nodejs支持情况

  • 将文件的扩展名由 .js 改为 .mjs;

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

  • esm 可以加载内置模块

    import fs from 'fs'
    fs.writeFileSync('./foo.txt','es modules works')
    复制代码
  • 对于第三方的 NPM 模块也可以通过 esm 加载

    import _ from 'lodash'
    console.log(_.camelCase('ES MOdule'))
    复制代码
  • 不支持提取第三方模块内的成员,因为第三方模块都是导出默认成员

import {camelCase} from 'lodash'
console.log(camelCase('ES MOdule'))
//The requested module 'lodash' does not provide an export named 'camelCase'
复制代码
  • 也可以直接提取模块内的成员,内置模块兼容了 ESM 的提取成员方式

    import { writeFileSync } from 'fs'
    writeFileSync('bar.txt', '来测试提取内置成员')
    复制代码

在ESM中载入CommonJS模块

  • ES Module中可以导入CommonJS模块
import mod from './commonjs.js'
console.log(mod)
复制代码
  • CommonJS模块始终只会导出一个默认成员,也就是说ESM只能使用import默认成员的方式导出
  • 不能直接提取成员,注意import不是结构导出对象
import { foo } from './commonjs.js'
console.log(foo)
//The requested module './commonjs.js' does not provide an export named 'foo'
复制代码
  • 不能在CommonJS模块中通过require载入ES Module

ESM in NodeJs与CommonJS的差异

  • ESM 中没有模块全局成员require、module、exports、__filename、__dirname等
  • require, module, exports 自然是通过 import 和 export 代替
  • “__filename” 和 “__dirname” 通过 import 对象的 meta 属性获取

ESM in NodeJs新版本的进一步支持

  • 在package.json中添加type属性

    {
      "type": "module"
    }
    //当前文件夹下的js文件自动以ESM模式运行,但是commonJS的文件要使用.cjs扩展名,否则默认使用ESM模式运行
    复制代码

常用的模块化打包工具

存在的问题

  • ESM存在环境兼容问题
  • 模块文件过多,网络请求频繁
  • 所有的前端资源都需要模块化

打包工具的设想

  • 具有新特性的代码编译为浏览器都能运行的代码
  • 开发阶段模块化开发,生产环境打包成一个文件
  • 支持不同种类的资源类型

webpack 模块打包器

打包工具解决的是前端整体的模块化,并不单指JavaScript模块化

webpack 4之后支持零配置的方式直接启动打包,打包的方式会按照约定将src下的index.js作为打包的入口,打包的文件会存入dist下的main.js

webpack工作模式

这个属性有三种取值,分别是 production、development 和 none。

  • 生产模式下,Webpack 会自动优化打包结果;
  • 开发模式下,Webpack 会自动优化打包速度,添加一些调试过程中的辅助;
  • None 模式下,Webpack 就是运行最原始的打包,不做任何额外处理;

折叠vs code代码command+k command+0

webpack资源文件加载

webpack默认打包js文件,所以直接打包css文件时会报错

  module:{
    // 其他资源模块的加载规则配置
    rules:[
      // 需要配置两个属性
      {
        // 用来匹配打包的文件文件路径的正则表达式
        test:/.css$/,
        // 用来指定匹配的路径使用的loader
        /*
        安装style-loader是将css-loader转换之后的结果通过style标签插入到页面中
        如果use配置了多个loader,执行时是按照从后往前的顺序执行
        */
        use:[
          'style-loader',
          'css-loader'
        ]
      }
    ]
  }
复制代码

webpack 导入资源模块

  • 根据代码的需要动态导入资源文件

dataurl类型->二进制文件:图片和字体

url-loader

  • 小文件使用Data URLs,减少请求次数

  • 大文件单独提取存放,提高加载速度

          {
            test: /.png$/,
            use: {
              loader: 'url-loader',//使用url-loader还是要添加file-loader,超出限制大小的文件会自动加载file-loader
              options: {
                //超出10KB文件单独存放,小于10KC文件转换为DataURLs 嵌入代码中
                limit: 10 * 1024 // 长度为字节所以要*1024 10 KB
              }
            }
          }
    复制代码

webpack常用加载器分类

  • 编译转换类加载器css-loader,会把资源模块转换为javascript代码,从而去实现通过js运行css
  • 文件操作类型加载器,会把我们加载资源拷贝到输出目录,然后将这个资源的访问路径向外导出
  • 代码检查类加载器,用于同一代码风格,一般不会去修改代码

webpack处理ES2015

因为模块打包需要,所以才会处理import和export,不能转换代码中的其他es6特性

需要单独安装相应的loader

yarn add babel-loader @babel-core @babel/preset-env --dev
复制代码
  • webpack只是打包工具
  • 加载器可以用来编译转换代码

webpack模块加载方式

  • 遵循ESM标准的import声明

  • 遵循CommonJS标准的require函数

    //通过require函数导入一个ESM的话,对于ESM的默认导出我们需要通过导入结果的default属性去获取
    const creatHeadding = require("./heading.js").default
    复制代码
  • 遵循AMD标准的define函数和require函数

loader加载的非JavaScript也会触发资源加载

  • 样式代码中的@import指令和url
  • html代码中图片标签的src属性
      {
        test: /.html$/,
        use: {
          loader: 'html-loader',
          options: {
            //默认情况下webpack中只有img的src会触发资源加载,所以要通过arrts来配置触发的属性
            attrs: ['img:src', 'a:href']
          }
        }
      }
复制代码

webpack核心工作原理

一般的项目中会散落各种各样的代码及资源文件,webpack会根据我们的配置找到项目的打包入口,一般这个文件会是提个JavaScript文件。然后根据这个文件中出现的import或者require解析出所依赖的资源模块,然后解析每个资源模块对应的依赖,然后就形成了整个项目中依赖关系的依赖树,有了这个依赖树,webpack会递归这个依赖树,然后找到每个节点所对应的资源文件,根据我们在配置文件中的rules属性去找到模块对应的加载器,然后交给加载器去加载这个模块,最后会把加载后的结果放到bundle.js打包结果中,从而实现整个项目的打包。

整个过程中loader机制是webpack的核心

webpack开发一个loader

  • webpack的工作过程类似于一个管道,他的加载过程可以使用多个loader,最终这个工作管道的结果必须是一段JavaScript代码
  • 可以直接让loader返回JavaScript或者再用一个loader返回JavaScript

webpack插件机制

  • 增强webpack在项目自动化方面的能力,loader专注实现资源模块的加载,Plugin解决其他自动化工作
  • 例如:清除dist目录、拷贝静态文件至输出目录、压缩输出代码

自动生成HTML插件

yarn add html-webpack-plugin --dev
复制代码
  • 自动生成html文件

    plugins:[
      new HtmlWebpackPugin()
    ]
    复制代码
  • 生成多个html文件

//webpack.config.js文件
//创建index.html
new HtmlWebpackPlugin({
  title:'index文件',
  meta:{
    viewport:'width = device-width'
  },
  //配置index.html模板
  /*
  使用lodash模板语法配置模板
  <h1><%= htmlWebpackPlugin.options.title %></h1>
  */
  template:'./src/index.html'
})
//创建about.html
new HtmlWebpackPlugin({
  //filename指出输出的文件名
  filename:'about.html'
})
复制代码

静态文件输出

可以使用copy-webpack-plugin

const CopyWebpackPlugin = require('copy-webpack-plugin')

module.export={
  plugins:[
    new CopywebpackPlugin([
      //这里可以是文件目录或者是通配符
      //'public/**',
      'public'
    ])
  ]
}
复制代码

webpack插件是在声明周期的钩子中挂在函数实现扩展

webpack开发体验

  • 使用http serve去运行
  • 能够自动编译+自动刷新
  • 提供sourceMap支持

webpoack自动编译+自动刷新

watch模式

/*为了挺高构建效率并没有打包出新的文件,而是将文件暂时写入磁盘当中
webpack默认将打包输出文件全部作为资源文件,只要是webpack打包输出的文件都可以被访问到
如果有其他的静态资源需要被访问,这需要单独进行配置
*/
yarn webpack-dev-server --dev
复制代码
const CopyWebpackPlugin = require("copy-webpack-plugin")
module.exports={
  //用于指定静态资源输出目录,一般用于开发过程中减少资源的拷贝等工作
  devServer:{
    contentBase:[
      'public'
    ]
  },
  plugins:[
    //一般这种使用方式用于上线前的打包,而不是用于开发过程中,会有额外开销
    //new CopyWebpackPlugin(["public"])
  ]
}
复制代码

webpack Dev Server 代理API

module.exports={
  devServer:{
    //代理模式
    proxy:{
      '/api':{
        /*
        http://localhost:8080/api/users-相当于请求了>https://api.github.com/api/users
        */
        target:'https://api.github.com',
        //重写代理路径
        pathRewrite:{
        /*
        http://localhost:8080/api/users-相当于请求了>https://api.github.com/users
        以正则的方式匹配相应的地址为空
        */
          '^/api':''
        },
        //值为true时是以实际代理主机名进行请求
        changeOrigin:true
      }
    }
  }
}
复制代码

sourceMap

源代码地图,用于映射源代码和转换代码之间的关系,用于开发环境中的调试。

解决了源代码与运行代码不一致产生的问题

共有12中模式

module.exports={
  devtool:'source-map'
}
复制代码

eval(‘consol.log(“123”)’)eval可执行一段js代码。不会生成sourceMap文件,只能定位文件,不能定位错误发生的行列

  • eval-是否使用eval执行模块代码
  • cheap-source Map是否包含行信息
  • module-是否能够得到loader处理之前的源代码
  • inline
  • no sources

如何选择sourceMap模式

开发环境选择cheap-module-source-map原因

生产环境选择none

了解不同模式的差异,适配不同的环境。

开发行业没有绝对的通用法则

Hot Module Replacement HMR 模块热更新

运行过程中的及时变换

是webpack中最强大的功能之一,极大的提高了开发效率

webpack-dev-server --hot//开启特性
复制代码
//要配置两个地方
const webpack = require('webpack')
module.exports={
  //1.在配置文件中进行配置开启
  devServer:{
    hot:true
  }
  plugins:[
  //2.在插件中进行添加
  new webpack.HotModuleReplacePlugin()
  ]
}
复制代码

HMR开启后需要手动处理JS模块热更新后的热替换

module.hot.accept('./editor',()=>{
  //手动存储模块状态
})
const img = new Image()
//图片热更新,主要是存储图片的路径即可
module.hot.accept('./better.png',()=>{
  img.src = background
})
复制代码

HMR注意事项

  • 处理HMR的代码报错会导致自动刷新

    const webpack = require('webpack')
    module.exports={
      devServer:{
        //这里使用hotOnly,发生错误时代码就不会自动刷新
        hotOnly:true
      }
      plugins:[
      new webpack.HotModuleReplacePlugin()
      ]
    }
    复制代码
    • 没有开启HMR产生报错,就要先判断一下是否已经开启

      if(module.hot){
         module.hot.accept('./editor',()=>{
        //手动存储模块状态
      })
       }
      复制代码

webpack生产环境优化

  • 生产环境注重运行效率
  • 开发环境注重开发效率

不同环境下的配置

  • 配置文件根据环境不同导出不同的配置,适合中小型项目
      module.exports=(env,argv)=>{
        //env通过cli传递的环境参数,argv是运行环境中的所有参数
        const config ={}//开发环境配置的内容
        if(env === 'production'){
           config.mode = 'production'
          config.devtool = false//关闭sourceMap
          config.plugins = [
            ...config.plugins,
            new CleanWebpackPlugin(),
            new CopyWebpackPlugin(['public'])
          ]
        }
        return config
      }
复制代码
  • 一个环境对应一个配置文件
    //这样需要有三个文件 webpack.common.js/webpack.dev.js/webpack.prod.js
    //webpack.prod.js
    const common = require('./webpack.common.js')
    const merge = require('webpack-merge')//主要用于处理webpack的配置文件合并
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    const CopyWebpackPlugin = require('copy-webpack-plugin')
    mpdule.export=merge(common,{
      mode:'production',
      plugins:[
        new CleanWebpackPlugin(),
        new CopyWebpackPlugin(['public'])
      ]
    })
复制代码
yarn webpack --config webpack.prod.js
复制代码

DefinePlugin 为代码注入全局成员

在production模式下这个插件会默认启动,在环境中注入process.env.NODE_ENV的常量

const webpack = require('webpack')
module.exports={
  plugins:[
    new webpack.DefinePlugins({
      //这个常量会自动注入到我们的代码当中,在main.js中可以直接使用
      API_BASE_URL:'"https://api.example.com"'
    })
  ]
}
复制代码

Tree-Sharking

在生产模式会自动开启,是一组功能搭配使用的效果

module.exports={
  //集中处理优化功能
  optimization:{
    //只导出外部使用的功能
    usedExports:true,//负责标记【枯树叶】
    concatenateModules:true//尽可能的将所有的模块合并输出到一个函数中,既提升了运行效率,又减少了代码的体积。这个功能又被成为Scope Hosting 作用域提升
    minimize:true//负责【摇掉】它们
  }
}
复制代码

TreeShaking使用的前提是使用ESM组织代码,由Webpack打包的代码必须使用ESM,如果配置中使用了babel低版本可能会将ESM转为CommonJS的方式,所以TreeShaking就会不生效,新版本babel中已经关闭了这个转换功能,所以TreeShaking还是可以继续使用的

module.exports={
  module:{
    rules:[
      {
        test:/\.js$/,
        use:{
          loader:'babel-loader',
          opitons:{
            presets:[
              ['@babel/preset-env',{ modules:false }]//确保了preset-env中不会开启ESM的转换插件,保证了TreeShaking使用的前提
            ]
          }
        }
      }
    ]
  }
}
复制代码

SideEffects副作用

副作用用:模块执行时除了导出成员之外所做的事情

一般用于npm包标记是否有副作用

module.exports={
  optimization:{
    sideEffects:true//在production模式下自动开启,开启过后就会先检查当前代码所属的package.json中有没有sideEffects标识,以此来判断
  }
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享