# 模块化

# 模块化规范

三大 JavaScript 主流模块规范:CommonJS、AMD 和 ES6 Module。

# CommonJS

CommonJS (opens new window) 规范是 2009 年一名来自 Mozilla 团队的工程师 Kevin Dangoor 开始设计一个叫 ServerJS 的项目提出来的,随着 Node.js 的广泛应用,被广泛接受。通过 ServerJS 这个名字就可以知道,CommonJS 主要是服务端用的模块规范。

// sayhi.js
var hi = 'hello world';

function sayHi() {
    return hi;
}

module.exports = sayHi;
// index.js

var sayHi = require('./sayhi.js');

console.log(sayHi());

上面的代码就是 CommonJS 语法,使用了require来导入一个模块,module.exports导出模块。在 Node.js 中实际代码 (opens new window)会被处理成下面代码而被应用:

(function(exports, require, module, __filename, __dirname) {
    // ...
    // 模块的代码在这里
    // ...
});

CommonJS 的问题:Commonjs规范是不适合浏览器的,但由于有了打包工具的支撑,通过处理的Commonjs也可以在浏览器中使用。

CommonJS 规范是 JavaScript 中最常见的模块格式规范,这一标准的设计初衷是为了让 JavaScript 在多个环境下都实现模块化。

起先主要应用在 Node.js 服务端中,但是 Node.js 中的实现依赖了 Node.js 本身功能的实现,包括了 Node.js 的文件系统等,这个规范在浏览器环境是没法使用的。

后来随着 Browserify (opens new window) 和 Webpack 等打包工具的崛起,通过处理的 CommonJS 前端代码也可以在浏览器中使用。

# AMD

AMD 规范 (opens new window)是在 CommonJS 规范之后推出的一个解决 web 页面动态异步加载 JavaScript 的规范,相对于 CommonJS 规范,它最大特点是浏览器内支持、实现简单、并且支持异步加载,AMD 规范最早是随着RequireJS (opens new window)发展而提出来的,它最核心的是两个方法:

  • require():引入其他模块;
  • define():定义新的模块。

基本语法如下:

// sayhi.js
define(function() {
    var hi = 'hello world';
    return function sayHi() {
        return hi;
    };
});

// index.js
require(['./sayhi.js'], function(sayHi) {
    console.log(sayHi());
});

AMD 提出来之后,也有很多变种的规范提出来,比如国内 Sea.js (opens new window)CMD (opens new window),还有兼容 CommonJS 和 AMD 的 UMD 规范 (opens new window)(Universal Module Definition)。虽然 AMD 的模式很适合浏览器端的开发,但是随着 npm 包管理的机制越来越流行,这种方式可能会逐步的被淘汰掉。

AMD 规范的问题:依赖前置加大了开发的难度,无论在阅读代码还是编写代码的时,都会导致引入的模块内容是条件性执行的。

在 AMD 规范中,要声明一个模块,那么需要指定该模块用到的所有依赖项,这些依赖项会被当做形参传到 factorydefine方法传入的函数叫做factory)中,对于依赖的模块会提前执行,这种做法叫做 依赖前置。依赖前置加大了开发的难度,无论在阅读代码还是编写代码的时,都会导致引入的模块内容是条件性执行的。

而且不管 AMD 还是 CommonJS 都没有统一浏览器和客户端的模块化规范。

# ES6 Module

ES6 Module,又称为 ES2015 modules,是 ES2015 标准提出来的一种模块加载方式,也是 ECMAScript 官方提出的方案,作为 ES 标准,不仅仅在 Web 现代浏览器(例如 Chrome)上面得到实现,而且在 Node.js 9+ 版本也得到原生支持(需要加上--experimental-modules使用)。

// sayhi.js
const hi = 'hello world';
export default function sayHi() {
    return hi;
}

// index.js
import sayHi from './sayhi';
console.log(sayHi());

对于前端项目,可以通过 Babel 或者 Typescript 进行提前体验。

随着主流浏览器逐步开始支持 ES Modules(ESM) 标准,越来越多的目光投注于 Node.js 对于 ESM 的支持实现上。

目前 Node.js 使用 CommonJS 作为官方的模块解决方案,虽然内置的模块方案促进了 Node.js 的流行,但是也为引入新的 ES Modules 造成了一定的阻碍。不过 Node.js 9.0+ 已经支持 ESM 语法,看示例:

// sayhi.mjs
const hi = 'hello world';
export default function sayHi() {
    return hi;
}

// index.mjs
import sayHi from './sayhi.mjs';
console.log(sayHi());

// 使用命令
➜ node --experimental-modules index.mjs
hello world

需要添加 flag --experimental-modules 来启动 ESM 语法支持,文件则必须使用 .mjs 后缀: node --experimental-modules some-esm-file.mjs

image-20210803114427025

# Webpack工作原理简析

在 Web 前端,不仅仅只有 JavaScript,还有 CSS、HTML、图片、字体、富媒体等众多资源,还有一些资源是以类似「方言」的方式存在着,比如 less、sass、各种 js 模板库等,这些资源并不能被直接用在 JavaScript 中,如果在 JavaScript 中像使用模块一样使用,那么可以极大的提高的开发体验:

var img = require('./img/webpack.png');
var style = require('./css/style.css');
var template = require('./template.ejs');

这时候,就需要 Webpack 了!在 Webpack 中,一切皆模块!

webpack的编译逻辑:

  • Webpack 会要对整个代码进行静态分析,分析出各个模块的类型和它们依赖关系
  • 将不同类型的模块提交给对应的加载器(loader)来处理。

比如:一个用 Less 写的样式,可以先用 less-loader 将它转成一个 CSS 模块,然后再通过 css-loader 把他插入到页面的 <style> 标签中执行,甚至还可以通过插件将这部分 CSS 导出为 CSS 文件,使用link标签引入到页面中。

# webpack对Module 的增强

在 Webpack 中,不仅可以为所欲为的使用 CommonJS 、AMD 和 ES6 Module 三大规范(比如一个文件中混合使用三种规范),还可以使用 Webpack 对 Module 的增强方法和属性。

# import()和神奇注释

在 Webpack 中,import不仅仅是 ES6 Module 的模块导入方式,还是一个类似require的函数(其实这是 ES2015 loader 规范 (opens new window)的实现),可以通过import('path/to/module')的方式引入一个模块,import()返回的是一个Promise对象:

import('path/to/module').then(mod => {
    console.log(mod);
});

参考先前的一章,来创建基础的ES6的webpack项目。

下面看看使用import()和 import 的打包有什么区别:

// hello.js
export default 'hello';

// lazy.js
export default 'lazy module';

// index.js
import hello from './hello';
import('./lazy').then(lazy => {
    console.log(lazy);
});

执行下命令:

npx webpack --mode development:

image-20210804150238493

通过打包后的 log 和dist文件夹内容发现,的代码被分割成了两个文件,一个是main.js一个是lazy_js.js,这是因为相对于import from的静态分析打包语法,import()是动态打包语法,即的内容不是第一时间打进main.js的,而是通过异步的方式加载进来的。代码分割是 webpack 进行代码结构组织,实现动态优化的一个重要功能

import()用法一样的是require.ensure的方法,这个方法已经被import()方式替换掉;

针对import()打包产物跟普通的静态分析打包的实现不同之处,后面原理篇讲解打包产出物的时候会详细介绍。

下面再来看下import()神奇注释特性,上面index.js的代码修改成下面这样,增加注释webpackChunkName: 'lazy-name'

import hello from './hello';
import(
    /*
     webpackChunkName: 'toimc-lazy'
     */
    './lazy'
).then(lazy => {
    console.log(lazy);
});

则打包后的结果,lasy_js.js变成了toimc-lazy.js了,这个文件的名字就是在import()注释里面指定的webpackChunkName,这就是神奇注释(Magic Comments)。

image-20210804150440531

目前支持的注释有:

  • webpackInclude:如果是 import 的一个目录,则可以指定需要引入的文件特性,例如只加载 json 文件:/\.json$/
  • webpackExclude:如果是 import 的一个目录,则可以指定需要过滤的文件,例如 /\.noimport\.json$/
  • webpackChunkName:这是 chunk 文件的名称,例如 toimc-lazy;
  • webpackPrefetch: 是否预取模块,及其优先级,可选值true、或者整数优先级别,0 相当于 true,webpack 4.6+支持;
  • webpackPreload: 是否预加载模块,及其优先级,可选值true、或者整数优先级别,0 相当于 true,webpack 4.6+支持;
  • webpackMode: 可选值lazy/lazy-once/eager/weak
import hello from './hello';

console.log(hello)

import(
  /* webpackChunkName: 'toimc-lazy' */
  /* webpackInclude: /\.json$/ */
  /* webpackExclude: /\.noimport\.json$/ */
  /* webpackMode: "lazy" */
  /* webpackPrefetch: true */
  /* webpackPreload: true */
  './lazy').then(lazy => {
    console.log(lazy);
});

这里最复杂的是webpackMode

  • lazy:是默认的模式,为每个 import() 导入的模块,生成一个可延迟加载 chunk;
  • lazy-once:生成一个可以满足所有 import() 调用的单个可延迟加载 chunk,此 chunk 将在第一次 import() 调用时获取,随后的 import() 调用将使用相同的网络响应;注意,这种模式仅在部分动态语句中有意义,例如 import(./locales/${language}.json),其中可能含有多个被请求的模块路径;
  • eager:不会生成额外的 chunk,所有模块都被当前 chunk 引入,并且没有额外的网络请求。仍然会返回 Promise,但是是 resolved 状态。和静态导入相对比,在调用 import() 完成之前,该模块不会被执行。
  • weak:尝试加载模块,如果该模块函数已经以其他方式加载(即,另一个 chunk 导入过此模块,或包含模块的脚本被加载)。仍然会返回 Promise,但是只有在客户端上已经有该 chunk 时才成功解析。如果该模块不可用,Promise 将会是 rejected 状态,并且网络请求永远不会执行。当需要的 chunks 始终在(嵌入在页面中的)初始请求中手动提供,而不是在应用程序导航在最初没有提供的模块导入的情况触发,这对于 Server 端渲染(SSR,Server-Side Render)是非常有用的。

通过上面的神奇注释,import()不再是简单的 JavaScript 异步加载器,还是任意模块资源的加载器,举例说明:如果页面用到的图片都放在src/assets/img文件夹下,你们可以通过下面方式将用到的图片打包到一起:

import(/* webpackChunkName: "image", webpackInclude: /\.(png|jpg|gif)/ */ './assets/img');

prefetch 优先级低于 preload,preload 会并行或者加载完主文件之后立即加载;prefetch 则会在主文件之后、空闲时在加载。prefetch 和 preload 可以用于提前加载图片、样式等资源的功能。

# require.resolve()require.resolveWeak()

require.resolve()require.resolveWeak()都可以获取模块的唯一 ID(moduleId),区别在于require.resolve()会把模块真实引入进 bundle,而require.resolveWeak()则不会,配合require.cache__webpack_modules__可以用于判断模块是否加载成功或者是否可用。

if (__webpack_modules__[require.resolveWeak('module')]) {
  // 当模块可用时,做一些事情...
}
if (require.cache[require.resolveWeak('module')]) {
  // 当模块被加载之前,做一些事情...
}

// 可以执行动态解析("context")
// 与其他 require/import 方法类似。
const page = 'Foo';
__webpack_modules__[require.resolveWeak(`./page/${page}`)];

# require.context()

require.context(directory, includeSubdirs, filter)可以批量将directory内的文件全部引入进文件(这个在视频中有介绍到),并且返回一个具有resolve的 context 对象,使用context.resolve(moduleId)则返回对应的模块。

  • directory:目录 string;
  • includeSubdirs:是否包含子目录,可选,默认值是 true;
  • filter:过滤正则规则,可选项。

Tips:注意 require.context() 会将所有的文件都引入进 bundle!

# require.include()

require.include()已被废弃,不久将被删除。

require.include(dependency)顾名思义为引入某个依赖,但是并不执行它,可以用于优化 chunk,例如下面示例代码:

require.include('./hello.js');
require.ensure(['./hello.js', './weak.js'], function(require) {
    /* ... */
});
require.ensure(['./hello.js', './lazy.js'], function(require) {
    /* ... */
});

# 资源的模块化处理

Webpack 还对一些常用的 Node.js 模块和属性进行了 mock,例如:在 web 的 js 文件中可以直接引入 Node.js 的querystring模块,这个模块实际引入的是node-libs-browser (opens new window)来对 Node.js 核心库 polyfill,详细在 web 页面中可以用的 Node.js 模块,可以参考node-libs-browser (opens new window) Readme 文件的表格。

# @importimport

在 Webpack 中的 css (包括其预处理语言,例如 Less、Sass)等,都可以在内部通过@import语法直接引入使用:

@import 'vars.less';
body {
    background: @bg-color;
}

除了样式文件中的@import,在 JavaScript 文件中,还支持直接使用 ES6 Module 的import(包括 require)直接引入样式文件,例如:

import styles from './style.css';

JavaScript 的这种语法其实是 CSS Modules 语法 (opens new window),目前浏览器支持程度有限,但是在 Webpack 中,可以通过配置 loader 优先使用这种方式!

后面讲解 CSS 样式配置的时候会更加详细的讲解 CSS Modules

# 把资源作为模块引入

在 Webpack 中,除了 CSS 可以直接使用import语法引入,类似 HTML 和页面模板等,可以直接使用对应的 loader ,通过下面的语法来引入:

const html = require('html-loader!./loader.html');
console.log(html);

上面的代码得到html变量实际为引入的loader.html的 string 片段。除了 html,类似模板文件,还可以直接转换为对应的 render 函数,例如下面代码使用了ejs-loader (opens new window)

const render = require('ejs!./file.ejs');
// => 得到 ejs 编译后的 render 函数
render(data); // 传入 data,直接返回的是 html 片段

原理篇将动手手写一个 markdown-loader ,更深入的了解其功能和原理实现。

后续的一章,将会介绍webpack中重要的loaders处理,webpack借助loaders来处理各式各样的资源。