模块化开发

Node.js 是 JavaScript 的服务器运行环境(runtime)。它对 ES6 的支持度更高。我们一般会使用Node来运行ES6代码。(现在的大部分主流浏览器已经支持ES6了)

我们可以直接使用如下命令来运行一个js文件:

node index.js

除了这些,我们也可以将ES6转化成ES5,这样就可以很好的兼容一些老的浏览器:

此时就需要用到ES6 转码器 babel

在学习ES6之前,我们可以学习一些npm,babel,polyfill, gulp,webpack之类的编译打包工具,和commonjs,seajs,requirejs等模块化规范,帮助我们更好的理解现下的模块化开发体系。

下面包含相关技术的简单解释:

npm 参考

我们已经知道了node是什么,而我们装node环境的时候,也会自动装好npm。npm全称:

Node Package Manager(Node包管理)学过Java的知道有个maven,.net 有个Nuget,python有个pip,都和npm差不多。

我们可以通过NPM尽情的管理我们的包,可以配置我们的package.json来添加依赖,可以通过npm命令来添加依赖。这样你想要bootstrap,ant-design,echart等三方库的时候只需要一行命令就可以解决。

npm config get prefix
// 查看全局node_modues的目录

babel 参考

Babel 是一个 JavaScript 编译器

帮你把ES6语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境

Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如IteratorGeneratorSetMapProxyReflectSymbolPromise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。

举例来说,ES6 在Array对象上新增了Array.from方法。Babel 就不会转码这个方法。如果想让这个方法运行,可以使用core-jsregenerator-runtime(后者提供generator函数的转码),为当前环境提供一个垫片。

下面列出的是 Babel 能为你做的事情:

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过引入第三方 polyfill 模块,例如 core-js
  • 源码转换(codemods)

// Babel 输入: ES2015 箭头函数 [1, 2, 3].map(n => n + 1);

// Babel 输出: ES5 语法实现的同等功能 [1, 2, 3].map(function(n) { return n + 1; });

babel使用插件的形式帮我们处理不同的特性,比如你可以安装

@babel/preset-react来编译react代码

@babel/preset-typescrip帮你编译typescript代码

安装完对应的包之后,我们需要在对应的babel配置文件中添加这些插件就可以了:

.babelrc 或 babel.config.json

presets: [ '@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript' ]

babel也支持自定义插件,利用 astexplorer.net 可以立即创建一个插件,或者使用 generator-babel-plugin 生成一个插件模板。

由于 Babel 支持 Source map,因此你可以轻松调试编译后的代码。

polyfill

polyfill就是我们常说的补丁代码,为了适应老版浏览器兼容性问题。我们必须给这些老版浏览器添加一些ES3~ES6的特性支持,于是引入了polyfill的概念。

这里面又涉及@babel/preset-env 、@babel/polyfill@babel/transform-runtime@babel/runtime 以及 core-js

总的来说,打补丁主要有三种方法:

  1. 手动打补丁
  2. 根据覆盖率自动打补丁
  3. 根据浏览器特性,动态打补丁

例如IE11不支持 Object.assign,可以进行如下手动打补丁,但是成本太大。

1. 可以使用三方库

Object.assign = require('object-assign')

2. 自己进行补丁

// Refer: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
if (typeof Object.assign != 'function') {
  // Must be writable: true, enumerable: false, configurable: true
  Object.defineProperty(Object, "assign", {
    value: function assign(target, varArgs) { // .length of function is 2
      'use strict';
      if (target == null) { // TypeError if undefined or null
        throw new TypeError('Cannot convert undefined or null to object');
      }

      var to = Object(target);

      for (var index = 1; index < arguments.length; index++) {
        var nextSource = arguments[index];

        if (nextSource != null) { // Skip over if undefined or null
          for (var nextKey in nextSource) {
            // Avoid bugs when hasOwnProperty is shadowed
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              to[nextKey] = nextSource[nextKey];
            }
          }
        }
      }
      return to;
    },
    writable: true,
    configurable: true
  });
}

在Webpack 的加持下,我们可以更现代化的方式打补丁:

1. @babel/preset-env  按需编译和按需打补丁

是根据参数 targets 来确定目标环境,默认情况下它编译为ES2015,可以根据项目需求进行配置:

 ...
presets: [
[
'@babel/preset-env',
{
// 支持chrome 58+ 及 IE 11+
targets: {
chrome: '58',
ie: '11',
}
},
],
]
...

具体 targets 参数可参见 browserlist.

2. core-js JavaScript 标准库

core-js 是实现 JavaScript 标准运行库之一,它提供了从ES3~ES7+ 以及还处在提案阶段的 JavaScript 的实现。

3. @babel/plugin-transform-runtime - 重利用 Babel helper 方法的babel插件

@babel/plugin-transform-runtime 是对 Babel 编译过程中产生的 helper 方法进行重新利用(聚合),以达到减少打包体积的目的。此外还有个作用是为了避免全局补丁污染,对打包过的 bunler 提供"沙箱"式的补丁。

4. @babel/polyfill - core-js 和 regenerator-runtime 补丁的实现库

@babel/polyfill 通过定制 polyfill 和 regenerator,提供了一个ES2015+ 环境 polyfill的库。因为它是由其他两个库实现的,直接引入其他两个库即可,所以已被废弃

// 实现 @babel/polyfill 等同效果
import 'core-js/stable'
import 'regenerator-runtime/runtime'

一般来说:

应用的补丁 - 使用@babel/preset-env + useBuiltIns

useBuiltIns 告诉了@babel/preset-env 如何根据应用的兼容目标(targets)来处理 polyfill

首先,在应用入口引入core-js:

import 'core-js'

然后,配置 useBuiltIns 参数为 entry,并指定core-js版本:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": 3,
"targets": {
"chrome": 58
} }
],
"@babel/preset-react"
]
}}

gulp 参考

gulp是一个工具包,可帮助您自动化开发工作流程中繁重而耗时的任务。

gulp旨在强调自动化前端构造流程,通过用户自定义配置一系列的任务(Task),并排列好顺序后执行,从而构建自动化流程。

gulp比较适合多页面应用开发,为通用website而生。webpack适合单页面应用开发,将资源模块化打包,适配各种模块系统,并且可以减少资源请求数量,从而减少应用程序必须等待的时间。

我们自定义的组件库,一般都会选择gulp来打包。

const gulp = require( 'gulp' ),
  babel = require( 'gulp-babel' ),
  uglify = require( 'gulp-uglify' ),
   minifyCss = require('gulp-clean-css'),
  webpack= require('vinyl-named');


/** 打包JS文件 */
async function compileESM() {
	return gulp
		.src(['packages/**/*.js'])
		.pipe(babel({ presets: ['@babel/preset-env'] }))
		.pipe(uglify())
		.pipe(gulp.dest('lib'));
}

/** 打包css*/
async function compileCss() {
	return gulp
		.src(['packages/**/*.css'])
		.pipe(minifyCss())
		.pipe(gulp.dest('lib'));
}

const build = gulp.parallel( compileESM, compileCss );

exports.build = build;
exports.default = build;

webpack 参考

webpack 是一个模块打包器。webpack 的主要目标是将 JavaScript 文件打包在一起,打包后的文件用于在浏览器中使用,但它也能够胜任转换(transform)、打包(bundle)或包裹(package)任何资源(resource or asset)。

从而我们可以看出webpack侧重的是模块化前端开发流程,就像分类管理的概念,将相同东西(例如css文件,js文件,图片文件)分类组成成单独的模块。

creat-react-app本质上使用的就是webpack打包。

module.exports = {
	/** 入口 (多个文件作为一个入口 全局使用babel-polyfill  polyfill将会被打包进这个入口文件中, 而且是放在文件最开始的地方)**/
	entry: {
		app: ['babel-polyfill', './src/main.js'],
	},
	publicPath: './',
	outputDir: 'dist', /** 打包时生成的生产环境构建文件的目录 **/
	assetsDir: 'public', /** 放置生成的静态资源(s、css、img、fonts)的(相对于 outputDir 的)目录(默认'') **/
	indexPath: 'index.html', /** 指定生成的 index.html 的输出路径(相对于 outputDir)也可以是一个绝对路径。 **/
	lintOnSave: false, /** 是否在保存的时候Eslint检查 */ 
	devServer: {
		port: 80,
		host: '0.0.0.0',
		open: true,
		index: '/index.html',
		overlay: {
			warnings: false,
			errors: false,
		},
		proxy: {}, /** 代理 */ 
	},
	productionSourceMap: false, /** 禁止生成sourceMap文件 **/
	chainWebpack: (config) => { 
		/** ....配置使用loader */
	},
};

CommonJS, AMD, CMD, UMD, ES6模块

CommonJS

2009年,美国程序员Ryan Dahl创造了Node.js 项目,将JavaScript语言用于服务器端编程。
这标志“JavaScript模块化编程”正式诞生。
Nodejs.的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有个全局性方法require(),用于加载模块。
Common.JS的Modules规范实现了一套简单易用的模块系统,CommonJS对模块的定义也十分的简单。主要分为模块定义、模块引用及模块标识三个部分。因为commonJs加载是同步加载的,不适合浏览器端加载。

 需要注意点

1) require第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。

2) CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

// a.js
module.exports = {
  name: 'ysl',
  age: '27'
}

// b.js
let obj = require('./a.js')
console.log(obj)  // { name: 'ysl', age: 27 }

RequireJS(AMD)

RequireJS是一个JavaScript模块加载器。它非常适合在浏览器中使用。
RequireJS是一个基于AMD规范实现的函数,它区别于传统的CommonJS的require规范。因为它能够异步地加载动态的依赖。
AMD ( Asynchronous Module Definition,译为异步模块定义)是一个在浏览器端模块化开发的规范。模块将被异步加载,模块加载不影响后面语句的运行。所有依赖某些模块的语句均放置在回调函数中。
AMD是Require. JS在推广过程中对模块定义的规范化的产出。

 // 先引入require.js
  <script src='../node_modules/requirejs/require.js' data-main='./index'></script>

  // moduleA.js
 define(function (){
   var add = function (x,y){
     return x+y;
   };
   return {
     add: add
   };
 });

  // index.js
 require(['v', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
    // some code here
       // 可以在这里编写模块加载后的代码
 });

 // require()函数接受两个参数:
 // 第一个参数是一个数组,表示所依赖的模块['moduleA', 'moduleB', 'moduleC']
 // 第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用
 // 注意['moduleA', 'moduleB', 'moduleC']这里面的三个模块与index.js在同一个目录

UMD

严格上说,umd不能算是一种模块规范,因为它没有模块定义和调用,这是AMD和CommonJS(服务端模块化规范)的结合体,保证模块可以被amd和commonjs调用。

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['moduleA'], factory);
    } else if (typeof module === 'object' && module.exports) {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory(require('moduleA'));
    } else {
        // Browser globals (root is window)
        root.returnExports = factory(root.moduleA);
    }
}(this, function (b) {
    //use b in some fashion.

    // Just return a value to define the module export.
    // This example returns an object, but the module
    // can return a function as the exported value.
    return {}
}));

SeaJS(UMD)

Seajs追求简单、自然的代码书写和组织方式,具有以下核心特性:

简单友好的模块定义规范: Seajs 遵循CMD规范,可以像Node;js - -般书写模块代码。
自然直观的代码组织方式:依赖的自动加载、配置的简洁清晰,可以让我们更多地享受编码的乐趣。
Seajs还提供常用插件,非常有助于开发调试和性能优化,并具有丰富的可扩展接口。
CMD ( Common Module Definition,译为通用模块定义)规范明确了模块的基本书写格式和基本交互规则。该规范是在国内发展出来的。
CMD是SeaJS在推广过程中对模块定义的规范化的产出。

// CMD
define(function(require, exports, module) {   
 var a = require('./a')   
 a.doSomething()   // 此处略去 100 行   
 var b = require('./b') // 依赖可以就近书写   
 b.doSomething()   // ... 
})

ES6模块

JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。
es6模块的语法分为两部分:export 模块导出、 import模块导入。

// profile.js 
var firstName = 'Michael';
export { firstName };

// main.js
import { firstName } from './profile.js';
  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。