JS综合解决方案5——模块化和构建工具

模块化是把一个复杂的程序,按照一定的规则或者规范拆分成若干模块

简单做个类比的话,模块就好比我们玩的乐高积木里面那一个个小零件

对于一个模块来说,它有内部的完整实现 ,对外则暴露一些接口可以和其他模块做交互

模块化历史

闭包

写过JS肯定都了解这个概念,它允许我们访问非当前函数作用域的变量,从而给不同的变量一个独有的命名空间 ,同时也允许了多个命名空间之间相互访问,这也是最早的一种模块化形式。

js 复制代码
function lastName() {
  const lastName = "ge"
  return () => {
    return lastName;
  }
}

function firstName() {
  const firstName = "poem"
  return () => {
    return firstName;
  }
}

// 借助闭包,简单粗暴地处理局部变量和依赖关系的问题
const name = lastName()() + '' + firstName()()

CMD&AMD&UMD

随着模块化工具的发展,过程中诞生了CMDAMDUMD三种模块化方案

现在还在库打包 的时候会用UMD,其他两种很少用了

AMD

  • 依赖于requirejs,专门跑在浏览器环境 中,主要关注异步加载
  • 使用define定义模块,require异步加载模块
js 复制代码
// 定义模块
define(['module1', 'module2'], function(module1, module2) {
  // 模块代码
  function init() {
    module1.foo();
    module2.bar();
  }

  // 导出模块
  return {
    init: init
  };
});

// 使用模块
require(['myModule'], function(myModule) {
  myModule.init();
});

CMD

  • 依赖seajs,专门跑在浏览器环境 中,更注重就近依赖
  • 使用define定义模块,require就近声明模块依赖关系
js 复制代码
// 模块定义
define(function(require, exports, module) {
  // 引入依赖模块
  var module1 = require('./module1');
  var module2 = require('./module2');

  // 模块代码
  function init() {
    module1.foo();
    module2.bar();
  }

  // 导出模块
  exports.init = init;
});

// 使用模块
var myModule = require('./myModule');
myModule.init();

UMD

  • 通用 模块化解决方案,兼容支持CMDAMD规范环境
  • 支持全局变量方式引入模块
  • 需要构建/打包工具生成代码
js 复制代码
(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['dependency1', 'dependency2'], factory);
  } else if (typeof exports === 'object' && typeof module === 'object') {
    // CommonJS
    module.exports = factory(require('dependency1'), require('dependency2'));
  } else {
    // 全局变量 (浏览器环境)
    root.MyModule = factory(root.Dependency1, root.Dependency2);
  }
}(typeof self !== 'undefined' ? self : this, function(dependency1, dependency2) {
  // 模块代码
  var myModule = {};

  function foo() {
    // 使用依赖模块
    dependency1.bar();
    dependency2.baz();
    console.log('foo');
  }

  myModule.foo = foo;

  return myModule;
}));

CommonJS和ES6 Modules

现在通用的模块化方案包括CommonJSES6 Modules,主要的区别是

  • CommonJS是node中的模块化方案,用在node端
  • ES6 Modules浏览器端 的模块化方案(简称ESM

CommonJS

CommonJSmodule.exports对外暴露当前的文件的内容,本质上,每个文件都有一个module变量,里面就存放了当前文件的路径exports属性

js 复制代码
Module {
  '8': [Function: internalRequire],
  id: '.',
  path: '/Users/shige/Desktop/NodeProjects/testFiles',
  exports: {},
  parent: null,
  filename: '/Users/shige/Desktop/NodeProjects/testFiles/test6.js',
  loaded: false,
  children: [],
  paths: [
    '/Users/shige/Desktop/NodeProjects/testFiles/node_modules',
    '/Users/shige/Desktop/NodeProjects/node_modules',
    '/Users/shige/Desktop/node_modules',
    '/Users/shige/node_modules',
    '/Users/node_modules',
    '/node_modules'
  ]
}

想要导入别的文件exports的内容,就需要用require引入,并且可以解构赋值

js 复制代码
const { hello } = require("../test.js")

ES6 Modules

ESM是专门的浏览器端 的模块化方案,和CommonJS的区别是,它的导入和导出都分了默认按需两种

按需导出/导入

exportimport关键字实现,对于按需导出的,导入的时候要用大括号,类似解构赋值的形式去做

js 复制代码
export function add(a, b) {
  return a + b
}

import { add } from "./calculate"
默认导出/导入

默认导出用export default,后面用大括号 把所有要导出的内容给包裹起来,相当于导出了一个对象

导入的时候,可以给这个默认导入重命名一下,使用其中的属性或者方法,用.去访问即可

js 复制代码
export default {
  name: "poem",
  add: function(a, b) {
    return a + b
  }
}

import test from "./calculate";
console.log(test.add);

模块化工具介绍

模块化发展到现在,出现了一些主流工具,包括:

  • webpack
  • vite
  • rollup

webpack

目前使用率最广 的前端打包工具,Vue的脚手架vue-cli和React脚手架CRA都是基于webpack

webpack的特点包括以下:

  • 打包从一个入口entry开始,以递归的形式找到所有依赖模块,构建出一个依赖关系图
  • 默认只能处理js文件,想处理其他类型文件,需要使用loader
  • 不同类型文件走不同流水线 ,最后输出结果,可以修改输出 output或者接入插件 plugin指定额外功能(打包优化、资源管理等)

webpack的缺点就是,当项目越大的时候,处理时间会越长,因为是要抓取构建整个应用,才能提供服务

vite

vite的核心和webpack有以下区别:

  • devServer开发服务器,基于原生ESM实现
  • 打包能力基于rollup

vite相比于webpack速度更快,因为在打包的时候把代码分成了依赖源码两种类型,只有当浏览器请求需要源码的时候再去转换并提供源码

rollup

一个专门的JS模块打包器 ,支持ES6模块原生treeshaking等一些优化功能,更偏向于库打包

模块化工具使用

webpack

课程里面提到的只有两个重点:configureWebpackchainWebpack,以及pluginloader区别

configureWebpack和chainWebpack

简单地说,configureWebpack简单的配置方式,不同于webpack官方,具体要看Vue文档提供的配置项

chainWebpack基于webpack-chain,是一个链式操作 的配置属性(有点像jquery),最后返回args配置属性

最基本的修改方式包括

  • tap:修改某个属性值
  • add:在属性值末尾添加新的值

例如我们可以通过配置项修改运行时项目的网页标签名称

js 复制代码
// 对于我们想修改的webpack配置项,可以在webpack-config.js里面找到要修改的配置项
// 具体链式操作时候的配置项,可以看看webpack-config.js的注释

// 这里以修改HTML的title为例
chainWebpack: (config) => {
  config.plugin('html').tap((args) => {
    args[0].title = '测试title'
      return args
    })
}

修改了之后可以用vue-cli里面的inspect命令输出webpack配置项,可以看到对应的配置项被修改了

js 复制代码
"scripts": {
  "inspect": "vue-cli-service inspect > webpack-config.js"
}

plugin和loader

和plugin以及loader有关的有三个术语

模块module:一般一个module为一个文件,通常指的是js文件

chunk:一个块是由打包工具通过配置生成的,包含了一组相关的模块

打包文件bundle:构建工具在打包过程中最终输出的文件,可在浏览器中运行

loader

loader是加载器 ,因为webpack默认只处理js文件,所以其他类型的文件需要loader处理

例如我们可以做这样的操作:对项目中的txt文件 的文本做替换,使用如下的loader

js 复制代码
const loadUtils = require("loader-utils");

module.exports = function (source) {
  const options = loadUtils.getOptions(this);
  const content = options.content;
  source = source.replace(/hello world/, content);
  return `module.exports = '${source}'`;
};

vue.config.js中添加如下的loader

js 复制代码
const { defineConfig } = require("@vue/cli-service");

module.exports = defineConfig({
  transpileDependencies: true,
  chainWebpack: (config) => {
    config.module
      .rule("txt-loader") // 规则名称
      .test(/\.txt$/) // 匹配文件
      .use("txt-loader")
      .loader("./src/loaders/textLoader") // loader地址
      .options({ content: "你好 哈哈" }) // 配置项,即后续更新的文本
      .end();
  },
});

最后就可以使用了

js 复制代码
const text = require("./test.txt");
console.log(text); // hello world会被替换成你好 哈哈
plugin

plugin是插件 ,可以为打包工具提供一些附加的功能

例如我们可以在编译完成的时候在控制台上添加一些输出,可以创建如下的plugin,里面需要带有一个apply方法,该方法会被自动执行

js 复制代码
class LogPlugin {
  constructor(options) {
    this.content = options.content;
  }

  // compiler是webpack编译器的实例
  apply(compiler) {
    // compiler里有若干生命周期,这里用了done表示编译结束
    compiler.hooks.done.tap("logPlugin", (compilation) => {
      console.log(this.content);
    });
  }
}

module.exports = LogPlugin;

接着就是在vue.config.js中添加如下的plugin

js 复制代码
const { defineConfig } = require("@vue/cli-service");
const LogPlugin = require("./src/plugins/logPlugin");

module.exports = defineConfig({
  transpileDependencies: true,
  chainWebpack: (config) => {
    config
      .plugin("LogPlugin") // 插件名字
      // 第一个参数是插件,第二个是传入的参数,因为使用的时候都是args[0].XXX
      .use(LogPlugin, [
        {
          content: "hello shi",
        },
      ])
      .end();
  },
});

项目运行时候就能看到编译成功后的输出了

vite

vite作为新的打包工具,其速度快主要靠这几个方面

  • 基于原生ESM
  • 按需编译:只有当使用到某个模块,才会进行编译
  • 模块预构建:对于非ESM的模块,会提前预构建 ,可以在node_modules/.vite/deps文件夹中看到

esbuild

vite使用的就是esbuild实现模块预构建,这个预构建其实就是把CommonJS这些模块编译成ESM,速度很快

相关推荐
知野小兔6 分钟前
【Vue】Keep alive详解
前端·javascript·vue.js
好奇的菜鸟12 分钟前
TypeScript中的接口(Interface):定义对象结构的强类型方式
前端·javascript·typescript
橘子味的冰淇淋~16 分钟前
全面解析 Map、WeakMap、Set、WeakSet
前端·javascript·vue.js
学习前端的小z28 分钟前
【前端】JavaScript中的字面量概念与应用详解
javascript
夏天想41 分钟前
vue路由的几种模式。有什么区别
前端·javascript·vue.js
家有狸花1 小时前
CSS笔记(二)类名复用
javascript·css·笔记
一条破秋裤1 小时前
关于使用天地图、leaflet、ENVI、Vue工具实现 前端地图上覆盖上处理的农业地块图层任务
前端·javascript·vue.js
徐同保1 小时前
web3.js + Ganache 模拟以太坊账户间转账
开发语言·javascript·web3
hummhumm1 小时前
第 38 章 -GO语言 事件驱动架构
java·javascript·后端·python·架构·golang·ruby
济南小草根2 小时前
参加面试被问到的面试题
java·面试