前端学习笔记(三)(持续性更新)

565 阅读8分钟

Part2-2 模块化开发与规范标准

一、模块化演变过程

  • Stage1 文件划分方式

    将每一个功能和相关的数据存放到不同的文件中,约定每一个文件都是一个独立的模块 ,通过script标签导入

    <script src="module-a.js"></script>
    <script src="module-b.js"></script>
    // module a 相关状态数据和功能函数
    var name = 'module-a'
    function method1 () {
      console.log(name + '#method1')
    }
    <script>
    // 命名冲突
    method1()
    // 模块成员可以被修改
    name = 'foo'
    </script>
    

    缺点:

    1.污染全局作用域。

    2.命名冲突问题。

    3.无法管理模块依赖关系

  • Stage2 命名空间方式

    所有的模块成员都挂载在一个对象上面

    module-a.js
    // module a 相关状态数据和功能函数
    var moduleA = {
      nameA: '',
      m1: function () {}
    }
    module-b.js
    var moduleB = {
      nameB: '',
      m2 : function () {}
    }
    

    缺点:

    1.模块成员仍然可以修改。

    2.无法管理模块依赖关系。

  • Stage3 IIFE:匿名函数自调用(闭包)

    为模块提供私有空间,将模块的每一个成员都放在一个函数提供的私有作用域之中。

    // module a 相关状态数据和功能函数
    ;(function () {
      var name = 'module-a'
      
      function method1 () {
        console.log(name + '#method1')
      }
      
      function method2 () {
        console.log(name + '#method2')
      }
    	// 将成员暴露给全局对象
      window.moduleA = {
        method1: method1,
        method2: method2
      }
    })()
    

    解决了私有作用域的问题,模块之间不能修改成员。

二、模块化规范

1.CommonJS规范:nodejs规范

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

CommonJS是以同步模式加载模块,node环境下可以完美运行,在浏览器端就不行,因为会存在异步任务。

2.AMD(Asynchronous Module Definition):异步模块的定义规范

诞生了require.js,同时又是一个很强大的模块加载器。

  • 每一个模块 都必须要通过define函数去定义。
  • define函数默认接收两个参数
// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
// 所以使用时必须通过 'jquery' 这个名称获取这个模块
// 但是 jQuery.js 并不在同级目录下,所以需要指定路径
// 参数1:模块名,模块2:依赖项,模块3:与参数2一一对应
define('module1', ['jquery', './module2'], function ($, module2) {
  return {
    start: function () {
      $('body').animate({ margin: '200px' })
      module2()
    }
  }
})
// 载入一个模块
require(['./module1'], function () {
  module1.start()
})

缺点:

  1. 使用起来相对复杂。
  2. 模块js文件请求频繁。

3.ES Modules

模块化的最佳实现:浏览器环境中使用ES Modules,node环境中使用Common JS

3.1 ES Modules的特性

  1. ESM 中自动使用严格模式,全局模式中的this为undefined,忽略'use strict'

    <script type="module">
      console.log(this); // undefined
    </script>
    <script>
      console.log(this); // window
    </script>
    
  2. 每一个ES module都是一个单独的作用域

    <script type="module">
      const a = 1
    console.log(a); // 1
    </script>
    <script type="module">
      console.log(a); // 报错
    </script>
    
  3. ESM是通过CORS的方式去请求外部的js模块的

    <!-- 正常访问 -->
    <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
    <!-- Access to script at 'https://libs.baidu.com/jquery/2.0.0/jquery.min.js' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. -->
    <script type="module" src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    
  4. ESM的script标签会延迟执行脚本

    <!-- 先弹框,后打印 -->
    <script src="./01.js"></script>  
    <p>哈哈哈哈哈哈</p> 
    <!-- 先打印,后弹框,相当于加了defer属性 -->
    <script type="module" src="./01.js"></script> 
    <p>哈哈哈哈哈哈</p>
    

3.2 ES Modules的核心功能

  1. 导出

    <script type="module" src="./app.js"></script>
    
    // 01.js
    // export const name = 'zs'
    
    // export function start () {
    //   console.log('start');
    // }
    
    const name = 'zs'
    
    function start () {
      console.log('start');
    }
    export { name, start } // export1
    // 如果使用as导出成员变量,引入的时候就要引入对应的name1,start1
    export { name as name1, start as start1 } // export2
    // 如果导出default
    export { name as default, start as start1 } // export3
    // export default name // export4
    
  2. 导入

    // app.js
    import { name, start } from './01.js' // import1
    // 对应as导出,导入对应的重命名之后的成员变量
    import { name1, start1 } from './01.js' // import2
    console.log(name1);
    start1()
    // 引入的default就要重命名,import3
    import { default as name1, start1 } from './01.js' // 写法一
    import name1, { start1 } from './01.js' // 写法二
    // 对应as导出
    console.log(name1);
    start1()
    // import name from './01.js' // import4
    
  3. 注意事项

    • 导出和引入的方式是基本语法。

      //导出  export { name, start }
      //导入 import { name, start } from './01.js'
      
    • 导出的是一个内存空间,而不是值的复制。

      let name = 'zs'
      let age = 18
      export { name, age }
      setTimeout(() => {
        name = 'ls'
      }, 1000)
      
      import { name, age } from './01.js'
      // 对应as导出
      console.log(name, age); // zs 18
      
      setTimeout(() => {
        console.log(name, age); // ls 18
      }, 1500)
      
    • 引入的成员是一个只读成员。

      import { name, age } from './01.js'
      name = 'wangwu' // app.js:6 Uncaught TypeError: Assignment to constant variable.报错
      // 对应as导出
      console.log(name, age); // zs 18
      
      setTimeout(() => {
        console.log(name, age); // ls 18
      }, 1500)
      
  4. 导入的用法

    // 1.不能省略.js后缀
    // import { name, age } from './01' // app.js:1 GET http://127.0.0.1:5500/part2-2/01 net::ERR_ABORTED 404 (Not Found)
    import { name, age } from './01.js'
    
    // 2.index.js不能省略
    // import { name, age } from './utils'
    import { name, age } from './utils/index.js'
    
    // 3../ 不能省略
    // import { name, age } from 'utils'
    import { name, age } from './utils/index.js'
    
    // 4.可以使用完整的路径或者url
    import { name, age } from 'http://127.0.0.1:5500/utils/index.js'
    
    // 5.直接导入模块使用
    import './utils/index.js'
    
    // 6.使用*导出模块所有成员
    import * as obj from './01.js' // Module {Symbol(Symbol.toStringTag): "Module"}
    
    // 7.动态导入
    import('./01.js').then(res => {
      console.log(res);
      // Module {Symbol(Symbol.toStringTag): "Module"}
    })
    
  5. ES Modules和CommonJS在node中的交互

    • ES Modules中可以导入CommonJS模块
    • CommonJS中不能导入ES Modules模块
    • CommonJS模块始终只会导出一个默认成员
    • 注意import不是结构导出对象,而是固定的语法

三、常用的模块化打包工具

1. webpack

webpack.config.js配置文件

entry

指定webpack打包文件的路径。

module.exports = {
  // 当对路径时,./不能省略
  entry: './src/main.js' 
}
output

设置输出文件位置。

  • filename:指定输出文件名称
  • path:指定输出文件所在目录 ,完整的绝对路径
const path = require('path')
module.exports = {
  ...
  // 输出文件
  output: {
    // 输出文件名
    filename: 'bundle.js',
    // 输出文件路径,完整的绝对路径
    path: path.join(__dirname, 'dist'),
  },
}
mode

设置打包环境,默认为production

  • none 不使用任何默认优化选项
  • production 使用生产配置优化选项
  • development 使用开发配置优化选项
module.exports = {
  // 打包环境
  mode: 'none',
}
loader

文件加载器,工作原理:负责资源文件从输入到输出的转换

css-loader

样式文件加载器,需要搭配style-css使用

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /.css$/,
        // 从后往前运行
        use: [
          // 将css挂载到style标签内
          'style-loader',
          'css-loader'
        ]
      }
    ]
	}
}
file-loader

文件资源加载器,加载img、font ,需要配置publicPath

module.exports = {
  ...
  // 指定文件加载目录 
  publicPath: 'dist/', 
  module: {
    rules: [
      {
        test: /.png$/,
        use: 'file-loader'
      }
    ]
	}
}
url-loader

使用Data URLs来表示文件,比如data:base64,适合小文件

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /.png$/,
        use: 'url-loader'
      }
    ]
	}
}
file-loader和url-loader的区别
  • 小文件使用url-loader,减少请求次数
  • 大文件单独提取存放,提高加载速度
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            // 超出10kb的文件单独存放,小于10kb文件转换为Data URLs嵌入代码中
            limit: 10 * 1024 // 10KB 
          }
        }
      }
    ]
	}
}
babel-loader

一般需要搭配安装babel核心模块@babel/core,具体特性转换插件集合@babel/preset-env

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /.js$/,
        use: {
          loader: 'babel-loader',
          options: {
          	// 指定转换插件结合,转换es6 
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
	}
}
html-loader

html文件加载器

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /.html$/,
        use: {
          loader: 'html-loader',
          options: {
          	// 指定img的src属性,a链接的href属性
            attrs: ['img:src', 'a:href']
          }
        }
      }
    ]
	}
}
Webpack模块的加载方式
  • 遵循ES Modules标准的import声明
  • 遵循CommonJS标准的require函数
  • 遵循AMD标准的define函数和require函数
  • 样式代码中的@import指令和url函数
  • HTML代码中的图片标签的src属性

2. webpack开发一个loader

开发一个md加载器

// webpack.config.js
module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /.md$/,
        use: [
          'html-loader', 
          './markdown-loader'
        ]
      }
    ]
  }
}
// markdown-loader.js
const marked = require('marked')
module.exports = source => {
  // console.log(source);
  // return 'console.log("hello ~")'
  const html = marked(source)
  // 1.返回一个js代码
  // return `module.exports = ${JSON.stringify(html)}`
  // return `export default ${JSON.stringify(html)}`
  // 2.返回一个html字符串,交给下一个loader处理
  return html
}

3. webpack插件

增强webpack 自动化能力,loader负责资源加载,plugin负责自动化工作,比如清除dist目录、拷贝静态资源到输出目录、压缩输出代码

  • clean-webpack-plugin 自动清理打包目录插件

    // yarn add clean-webpack-plugin --dev
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    module.exports = {
      ...
      plugins: [
        new CleanWebpackPlugin()
      ]
    }
    
  • html-webpack-plugin 自动生成使用打包结果的html插件

    // yarn add html-webpack-plugin --dev
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    module.exports = {
      ...
      plugins: [
        new CleanWebpackPlugin(),
        // 根据模板生成html,index.html
        new HtmlWebpackPlugin({
          // 自定义输出html
          title: 'Webpack Plugin Sample',
          meta: {
            viewport: 'width=device-width'
          },
          // 模板文件
          template: './src/index.html'
        }), 
        // 同时生成其他html,about.html
        new HtmlWebpackPlugin({
          filename: 'about.html'
        }),
      ]
    }
    
  • copy-webpack-plugin用于复制静态资源的插件

    // yarn add copy-webpack-plugin --dev
    const CopyWebpackPlugin = require('copy-webpack-plugin')
    module.exports = {
      mode: 'none',
      entry: './src/main.js',
      output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        // publicPath: 'dist/'
      },
      module: {
        ...
      },
      plugins: [
        new CleanWebpackPlugin(),
        // 根据模板生成html,
        new HtmlWebpackPlugin({
          title: 'Webpack Plugin Sample',
          meta: {
            viewport: 'width=device-width'
          },
          template: './src/index.html'
        }), 
        // 拷贝public中所有文件到dist目录
        new CopyWebpackPlugin([
          'public'
        ])
      ]
    }
    
  • 总结

    通过在生命周期的钩子中挂载函数实现扩展

    // 自定义插件
    class MyPlugin {
      apply (compiler) {
        console.log('myplugin qidong');
        compiler.hooks.emit.tap('MyPlugin', compilation => {
          for (let key in compilation.assets) {
            // console.log(key);
            console.log(compilation.assets[key].source());
            if (key.endsWith('.js')) {
              const contents = compilation.assets[key].source()
              const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
              compilation.assets[key] = {
                source: () => withoutComments,
                size: () => withoutComments.length
              }
            }
          }
        })
      }
    }
    

四、基于模块化工具构建现代Web应用

1.自动编译--watch

yarn webpack --watch

2.自动刷新浏览器--webpack-dev-server

yarn webpack-dev-server --open 打开浏览器

3.静态资源访问--devServer配置

module.exports = {
  devServer: {
  	// 指定额外的静态资源路径,字符串或者数组
  	contentBase: './public'
  }
}

4.devServer代理

注意点:If you're using webpack-cli 4 or webpack 5, change webpack-dev-server to webpack serve

module.exports = {
  devServer: {
  	// 指定额外的静态资源路径,字符串或者数组
  	contentBase: './public',
    proxy: {
      '/api': {
        // http://localhost:8080/api/user ---> https://api.github.com/api/users
        target: 'https://api.github.com',
        pathRewrite: {
          '^/api': ''
        },
        // 不使用本机主机名
        changeOrigin: true
      }
    }
  }
}

5.source-map 源代码地图

module.exports = {
  // 配置开发过程中的辅助工具 
  devtool: 'source-map'
}

5.1 不同类型的source-map对比:

image-20210330201026166.png

5.2 source-map选择:

  • dev环境:cheap-module-eval-source-map
    • 每行代码不超过80个字符,一行可以放下,很容易定位到位置。 -- cheap
    • 经过loader转换后的差异较大 。-- module
    • 首次打包速度慢无所谓,重写打包速度相对较快。 -- e va l
  • prod环境:none / nosources-source-map
    • 会暴露我们的源代码

6.HMR: hot-module-replace 热更新

需要手动去处理模块热替换的逻辑。

// 开启热替换功能
const webpack = require('webpack')
module.exports = {
  ...
  devServer: {
    hot: true
  }
  ...
  plugins: [
  	new webpack.HotModuleReplacementPlugin()
  ]
}

// main.js
let lastM = heading
// 处理热替换
if (module.hot) {
   module.hot.accept('./heading', () => {
    //  自定义的内容
    const val = lastM.innerHTML
    document.removeChild(lastM)
    const newM = new heading()
    newM.innerHTML = val 
    document.appendChild(newM)
    lastM = newM
  })
}
  • hotOnly替换hot,暴露报错
  • module.hot做判断
  • 热处理逻辑不会打包到项目里

7.DefinePlugin 为代码注入全局变量

module.exports = {
  ...
  plugins: [
    new webpack.DefinePlugin({
      API_BASE_URL: JSON.Stringify("https://XXX") // 代码片段,'"https://XXX"'
    })
  ]
}

五、打包工具的优化技巧

1. Tree-shaking

生产模式下自动开启,去掉 dead-code

// Tree-shaking的实现
module.exports = {
	optimization: {
    usedExports: true, // 只导出外部使用了的成员 
    minimize: true // 压缩代码
  },
}

注意:

Tree-shaking前提是ES Modules,babel-loader如果使用了preset-env工具函数对js进行转换(ES Modules ----> CommonJS),此时Tree-shaking 失效,最新版默认不会开启转换,所以不会失效。

module: {
    rules: [
      {
        test: /.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', { modules: true }] // 设置是否开启对es的转换
            ]
          }
        }
      },
    ]
}

2. Scope-hoisting 合并模块函数

// Tree-shaking的实现
module.exports = {
	optimization: {
    usedExports: true, // 只导出外部使用了的成员 
    concatenateModules: true, // 合并模块到一个中,减少代码体积
    // minimize: true, // 压缩代码
  },
}

3. sideEffexts副作用

副作用:模块执行时除了导出成员之外所做的事情。 一般用于开发 npm模块时才会用到

优点:可以提供更大的压缩空间。

// index.js
export a from './a'
export b from './b' // 对于main中来说这是多余的,也就形成了sideEffexts副作用
export c from './c' // 对于main中来说这是多余的,也就形成了sideEffexts副作用 

// main.js
import a from './index'

// webpack.config.js
module.exports = {
	optimization: {
    sideEffects: true
  },
}

// package.json
{
  ...
  // "sideEffects": false, // 确保项目中没有副作用的代码,才设置为false,没用到的就会tree-shaking掉
  "sideEffects": [
    "./src/a.js", // 标记哪些模块具有副作用,不会被tree-shaking掉
    "*.css"
  ]
}

4. Code-splitting 代码分割

4.1 多入口打包
module.exports = {
	entry: {
     a: './a.js',
     b: './b.js'
  },
  output: {
    filename: '[name].bundle.js' // [name]占位内容
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'a', // 标题
      template: './src/a.html', // 模板
      filename: 'a.html', // 文件名
      chunks: ['a'] // 分别设置chunks,html只会引入对应的打包后的js文件
    }),
    new HtmlWebpackPlugin({
      title: 'b',
      template: './src/b.html',
      filename: 'b.html',
      chunks: ['b'] // 分别设置chunks,html只会引入对应的打包后的js文件
    }), 
  ]
}
4.2 split chunks 提取公共模块
module.exports = {
	optimization: {
    splitChunks: {
      chunks: 'all' // 所有的公共模块会单独提取到一个文件中
    }
  },
}
4.3 按需加载
// 使用动态导入的方式实现按需加载,动态导入的模块会自动分包
import('/a').then({ a } => {
  // 处理逻辑
  ...
})
import('/b').then({ b } => {
  // 处理逻辑
  ...
})

// vue、react中可以使用动态路由懒加载的方式
4.4 魔法注释--magic comments
// import位置加入注释,相同的chunkname会打包到一起
import(/* webpackChunkName: 'a' */'/a').then({ a } => {
  // 处理逻辑
  ...
})
import(/* webpackChunkName: 'b' */'/b').then({ b } => {
  // 处理逻辑
  ...
})
4.5 MiniCssExtractPlugin 提取CSS到单个文件
// yarn add mini-css-exract-plugin --dev
const MiniCssExtractPlugin = require('mini-css-exract-plugin')
module.exports = {
	...
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          // 'style-loader', // 将样式通过style标签注入
          // css体积超过150k建议使用这种
          MiniCssExtractPlugin.loader, // 如果使用了MiniCssExtractPlugin插件,style-loader就没用了,通过link标签注入
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin()
  ]
}
4.6 optimize-css-assets-webpack-plugin css压缩插件
// yarn add mini-css-exract-plugin --dev
// yarn add optimize-css-assets-webpack-plugin --dev
// yarn add terser-webpack-plugin --dev js压缩插件
const MiniCssExtractPlugin = require('mini-css-exract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
	...
  // new OptimizeCssAssetsWebpackPlugin()放到这里,只有当minimizer开启的时候采取执行,也就是生产环境的时候才会执行
  optimization: {
    // 自定义压缩插件,会覆盖掉原有的插件 
     minimizer: [
       new TerserWebpackPlugin(), // js压缩
       new OptimizeCssAssetsWebpackPlugin() // css压缩
     ]
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          // 'style-loader', // 将样式通过style标签注入
          // css体积超过150k建议使用这种
          MiniCssExtractPlugin.loader, // 如果使用了MiniCssExtractPlugin插件,style-loader就没用了,通过link标签注入
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin(),
    // css文件压缩,所有环境都会执行这个压缩代码 
    // new OptimizeCssAssetsWebpackPlugin()
  ]
}
4.7 输出文件名 hash
  • [name]-[hash: 8] 项目级别,只要有一个地方改动hash值就会发生变化 ,: 8指定hash长度
  • [name]-[chunkhash: 8] 同一个chunk下的才会发生变化 ,: 8指定hash长度
  • [name]-[contenthash: 8]文件级别的hash,不同的文件不同的hash ,: 8指定hash长度

[name]-[contenthash: 8]最好的选择

// 可以缓存静态资源
const MiniCssExtractPlugin = require('mini-css-exract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
  output: {
    // filename: '[name]-[hash].bundle.js' // [name]-[hash] 项目级别,只要有一个地方改动hash值就会发生变化 
    // filename: '[name]-[chunkhash].bundle.js' // [name]-[chunkhash] 同一个chunk下的才会发生变化
    filename: '[name]-[contenthash].bundle.js' // [name]-[contenthash]文件级别的hash,不同的文件不同的hash
  },
	...
  // new OptimizeCssAssetsWebpackPlugin()放到这里,只有当minimizer开启的时候采取执行,也就是生产环境的时候才会执行
  optimization: {
    // 自定义压缩插件,会覆盖掉原有的插件 
     minimizer: [
       new TerserWebpackPlugin(), // js压缩
       new OptimizeCssAssetsWebpackPlugin() // css压缩
     ]
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          // 'style-loader', // 将样式通过style标签注入
          // css体积超过150k建议使用这种
          MiniCssExtractPlugin.loader, // 如果使用了MiniCssExtractPlugin插件,style-loader就没用了,通过link标签注入
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name]-[hash].bundle.css'
    }),
    // css文件压缩,所有环境都会执行这个压缩代码 
    // new OptimizeCssAssetsWebpackPlugin()
  ]
}