# 模块化
# 模块化规范
三大 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 规范中,要声明一个模块,那么需要指定该模块用到的所有依赖项,这些依赖项会被当做形参传到
factory
(define
方法传入的函数叫做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
。
# 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:
通过打包后的 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)。
目前支持的注释有:
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 文件的表格。
# @import
和import
在 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来处理各式各样的资源。