NODE_ENV

以前一直搞不懂,为什么package.json中配置scripts的时候为什么前面经常会有一个NODE_ENV=development或者NODE_ENV=production,自己起的项目里执行node文件就直接node xxx.js,webpack就直接webpack-dev-server --watch,因为一直比较懒,也没有看过相关的内容(工作中没有自己起过项目),而最近想要用node起一个博客后端项目,顺便学习node和ssr了。所以今天才来看看这块。

NODE_ENV其实就是Node.js暴露给执行脚本的系统环境变量。一般情况下就是用于区分当前是"production(生产环境)"还是"development(开发环境)",以此来让代码搞不同的事情。NODE_ENV会赋值给process.env对象。比如:

if (process.env.NODE_ENV === 'development') {
  // 开发环境需要做点什么,比如启动devserver...
} else {
  // 生产环境需要做点什么,比如运行webpack打包文件...
}

注意事项:

Webpack4以前,webpack.config.js中可以使用process.env.NODE_ENV,但是不能在webpack.config.js引入的模块中使用,要想在模块中使用需要使用DefinePlugin

new webpack.DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
})

webpack4以后可以通过mode选项实现。

还有通过npm script定义的NODE_ENVwebpack.config.js中定义的NODE_ENV是两个互相独立的存在,NODE_ENV=development这种方式只能在当前脚本中生效。是个runtime(运行时)。

假设webpack.config.js中定义的NODE_ENV为production,而脚本中执行NODE_ENV=development,那么在模块中NODE_ENV的值为production,而配置文件webpack.config.js中的NODE_ENV值为development。

如果脚本中没有配置NODE_ENV,webpack中的process.env.NODE_ENV就是undefined

其实这块意思就是说,webpack中的mode和插件选项定义的NODE_ENV作用于webpack入口文件下的业务代码。而npm脚本里的设置多用于配置相关,例如webpack.config,js里区分环境配置不同插件。

cross-env

cross-env就是一款跨平台设置和使用环境变量的脚本。

出现原因

当您使用NODE_ENV =production, 来设置环境变量时,大多数Windows命令提示将会阻塞(报错)。 (异常是Windows上的Bash,它使用本机Bash。)同样,Windows和POSIX命令如何使用环境变量也有区别。 使用POSIX,您可以使用:$ ENV_VAR和使用%ENV_VAR%的Windows。
说人话:windows不支持NODE_ENV=development的设置方式。

使用方法

{
  "scripts": {
    "build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"
  }
}

为什么需要模块化

  • web sites慢慢向web app转化,Javascript代码越来越庞大,越来越复杂
  • 网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试等等...不得不用软件工程的方法管理网页逻辑
  • 需要只关注核心业务逻辑,其他可以加载别人已经写好的模块,或者拆分一些通用模块

前端模块化历史

Es6以前,JavaScript不支持类(class),更不要说(module)了。而在没有类和模块概念以前,JavaScript模块化编程就出现了以下的发展历程:

一、原始写法

最早的时候,js代码写在html中的script标签中或者从script中的src引入到html文件中。这种写法会造成Global环境被污染,并且会造成命名冲突,模块间也看不出直接关系。

function foo() {
  // ...
}
function bar() {
  // ...
}

二、Namespace(命名空间)模式

简单封装解决了Global上变量数量过多的情况,但是对象暴露了模块所有成员,内部状态可以在外部被改写。也就是可修改内部变量,所以并不安全。

const myApp = {
  foo: function() {},
  bar: function() {},
  _count: 0
}
myApp._count = 1; // 可修改内部变量

三、匿名闭包:IIFE模式

使用立即执行函数,可以达到不暴露隐私成员的目的:

const module1 = (function() {
  const _count = 1;
  const foo = function() {
    console.log(_count);
  };

  return {
    foo
  };
})();
Module.foo(); // safe now
console.log(Module._count); // undefined

module1是Javascript模块的基本写法,有关IIFE闭包可以查看这里闭包,下面对这种写法进行加工。

四、模块模式,引入依赖

const module = (function($) {
  // 可以使用jQuery做点什么
  // ...
})(jQuery);
module.foo();

事实上这种模式也可以直接访问到全局中的jQuery,(沿作用链向上查找到window下的全局变量$)。用参数传入的方式,可以理解为让模块之间的关系变得明显。还有一点是,保证模块的独立性(我并没有理解这里的独立性说的是什么,毕竟传入的只是引用,大概是说模块接收传入模块会比较完整,而从从作用链向上查找会不完整吧)。

阮一峰老师这里写了三种模式,我认为可以是一个思考的过程,其实它们都是这一种模式。这种模式也就是模块模式,也是模块实现的基石。

以下是阮一峰老师叙述的几种模式:

(1)放大模式

如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"放大模式"(augmentation)

var module1 = (function (mod) {
  mod.m3 = function() {
    // ...
  };
  return mod;
})(module1);

这段执行的时候会报错,mod是undefined,module1当做参数传递给IIFE函数的时候还没有结果,所以给undefined.m3赋值当然是错误的。也就假设已经存在了module1这个模块,如下:

function foo() {
  // ...
}

const module1 = (function(mod) {
  foo.m3 = function() {
    // ...m3
  }
  return mod;
})(foo);

console.log(module1.m3); // f () { // ...m3 }

(2)宽放大模式(Loose augmentation)

在浏览器中,模块的各个部分通常都是从网上获取(CDN...),有时无法知道哪个部分先加载。如果采用上面的写法,第一个执行部分有可能加载一个不存在的空对象,这时就要采用"宽放大模式"。

var module1 = (function (mod) {
  mod.m3 = function() {
    // ...
  };
  return mod;
})(module1 || {});

与"放大模式"相比,"宽放大模式"就是"立即执行函数"的参数可以是空对象。

(3)输入全局变量

独立性是模块的重要特点,模块内部最好不要直接与程序的其他部分直接交互。

为了在模块内部调用全局变量,必须显示地将其他变量输入模块。

这也就是上面说的最终形态,模块模式。

const module1 = (function($, YAHOO) {
  // ...这里可以找到全局的jquery、Yui
})(jQuery, YAHOO);

模块规范

JavaScript没有模块规范,在阮一峰老师博客上一篇12年的文章看来,当时通行的规范有CommonJS和AMD规范。

CommonJS被NodeJS应用,并不被浏览器支持,原因是Commonjs是同步加载的,这样情况下在node服务端处理加载文件的时候读取本地文件是没有问题的,但是在浏览器上,读取服务器文件就会产生问题,因为加载文件的时间可能过长,导致浏览器假死。

所以AMD也应运而生,Asynchronous Module Definition(异步模块定义),AMD采用异步加载,所以模块的加载不影响后面的语句,当加载完成,回调函数才会运行。

ES6提出了模块概念。这期间还有玉伯提出的CMD规范,以及社区整合出的UMD(即整合CommonJS和AMD)规范,但是目前使用的比较少,所以没有继续了解下去了。以后有机会再继续深入了解。本文旨在学习CommonJS、ES6、稍了解AMD规范。

javascript-module-01

CommonJS

Node应用由模块组成,采用CommonJS规范。

每个文件就是一个模块,都有自己的作用域。在一个文件里定义的变量、函数、类都是私有的,对其他文件不可见。

也就是说:

const x = 5;

定义这样一个变量在其他文件中是无法访问的。

除非:

global.x = 5;

这样做可以在全局访问,但是并不推荐,模块的意义本身也有减少全局变量过多的这种情况。

CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外接口。加载某个模块,其实就是加载该模块的module.exports属性。

const x = 5;
module.exports = x;

上面通过module.exports输出变量x

require方法用于加载模块

const example = require('.example.js');
example.x; // 5
// 或者
const x = require('./example.js').x;
x; // 5

CommonJS模块的特点如下:

  • 所有代码都运行在模块作用域,不会污染全局作用域
  • 模块可以多次加载,但是只会在第一次加载时运行依次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。想让模块再次加载,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序(同步的)

module

Node内部提供一个Module构造函数,所有模块都是Module的实例。

每个模块内部,都有一个module对象,代表当前模块。有以下属性:

  • module.id 模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename 模块的文件名,带有绝对路径。
  • module.loaded 返回一个布尔值,表示模块是否已经完成加载。
  • module.parent 返回一个对象,表示调用该模块的模块。
  • module.children 返回一个数组,表示该模块要用到的其他模块。
  • module.exports 表示模块对外输出的值。

如果在命令行下调用某个模块,比如node something.js,那么module.parent就是null。如果是在脚本之中调用,比如require('./something.js'),那么module.parent就是调用它的模块。利用这一点,可以判断当前模块是否为入口脚本。

module.exports

module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。

exports

为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。

const exports = module.exports;

造成的结果是,在对外输出模块接口时,可以向exports对象添加方法。

exports.area = function (r) {
  // ...
};

// 相当于

module.exports.a = function () {
    console.log('ok')
}

注意,不能直接将exports变量指向一个值,因为这样等于切断了exportsmodule.exports的联系。

exports = function(x) {console.log(x)};

上面这样的写法是无效的,因为exports不再指向module.exports了。

下面的写法也是无效的。

exports.hello = function() {
  return 'hello';
};

module.exports = 'Hello world';

上面代码中,hello函数是无法对外输出的,因为module.exports被重新赋值了。

这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。

module.exports = function (x){ console.log(x);};

如果你觉得,exportsmodule.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports

exports和module.exports

  • exports是对module.exports的引用
  • require()返回的是module.exports而不是exports
  • module.exports的初始值是一个空对象{},所以exports也是空对象{}
  • module.exports的值不是对象的时候,exports的值始终是空对象{}

所以,exports不可以赋值,只能使用点语法添加属性,否则会导致跟module.exports之间的引用断裂。而module.exports可以赋值,也可以是任何类型,但是不可以在一个文件中多次module.exports,否则会以最后一个为准,这种情况应该直接赋值需要暴露的对象。如下:

// 写法一
module.epxorts = {
    a,
  b
}

// 写法二
module.exports.a = 1;
module.exports.b = 1;

// 写法三
exports.a = 1;
exports.b = 1;

所以想要一个模块直接输出一个函数不可以用exports,而应该使用module.exports

require

Node使用CommonJS模块规范,内置的require命令用于加载模块文件。

require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。

module.exports = function () {
    console.log('hello world');
};

require('./b')(); // hello world

上面代码中,require命令调用自身,等于是执行module.exports,因此会输出 hello world。

有关模块的加载规则、目录加载规则、模块缓存、环境变量、循环加载等问题由于工作中还未遇到,便没有深入了解。

ES6 module

ES6 在语言标准的层面上,实现了模块功能,而且非常简单,ES6到来,完全可以取代 CommonJS 和 AMD规范,成为浏览器和服务器通用的模块解决方案。由vue,Angular React这些mvvm 模式的框架发展,让前端的编程变得模块化,组件化。

特点:

  1. ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this。
  2. 自动采用严格模式"use strict"。须遵循严格模式的要求
  3. ES6 模块的设计思想是尽量的静态化,编译时加载”或者静态加载,编译时输出接口
  4. ES6 模块export、import命令可以出现在模块的任何位置,但是必须处于模块顶层。如果处于块级作用域内,就会报错
  5. ES6 模块输出的是值的引用

export命令

用于规定模块的对外接口,export命令除了输出变量,还可以输出函数或者类(class),不同类的数据类型:

// 变量
export const a = 1;
// 但是更推荐 
const a = 1;
const b = 2;

export {
    a,
  b
}
//这样可以写在末尾,也很容易看出输出了哪些变量

// 函数
export function add(a, b) {
    return a + b;
}

// 类class
export class class1 {}

// export输出的变量就是本来的名字,也可以用as关键字重命名
function v1() {return 1}
function v2() {return 2}

export {
    v1 as rename1,
    v2 as rename2,
      v3 as rename2
};
// 可以用不同名字输出两次

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

// 报错
export 1;

// 报错
var m = 1;
export m;

上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出1,第二种写法通过变量m,还是直接输出1。1只是一个值,不是接口。正确的写法是下面这样。

// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};

上面三种写法都是正确的,规定了对外的接口m。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。

另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

上面代码输出变量foo,值为bar,500毫秒之后变成baz

这一点与CommonJS规范完全不同。CommonJS模块输出的是值的缓存,不存在动态更新,详见下文《ES6模块加载的实质》一节。

最后,export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了ES6模块的设计初衷。

function foo() {
  export default 'bar' // SyntaxError
}
foo()

上面代码中,export语句放在函数之中,结果报错。

import命令

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

  • import命令具有提升效果,会提升到整个模块的头部,首先执行。
  • import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
  • import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js路径可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
  • 如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。
//静态加载,只加载export.js 文件中三个变量,其他不加载
import {a, add, rename1} from './export.js';

//import命令要使用as关键字,将输入的变量重命名。
import {add as add1} from './export.js';

//整体加载模块
improt * as all from './export.js'
all.foo()

export default命令

本质上,export default就是输出一个叫做default的变量或方法

// export-default.js
export default function foo() {
  console.log('foo');
}

// 或者写成

function foo() {
  console.log('foo');
}

//foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载
export default foo;


//import-default.js
import  myfoo from './export-default.js';

比较一下默认输出和正常输出

// 第一组 在外部这个命名是无效的,依然被当做匿名函数
export default function crc32() { // 输出
  // ...
}
// 这里的crc32相当于重新命名
import crc32 from 'crc32'; // 输入

// 第二组
export function crc32() { // 输出
  // ...
};

import {crc32} from 'crc32'; // 输入

分析:上面代码的两组写法,

第一组是使用export default时,对应的import语句不需要使用大括号;

第二组是不使用export default时,对应的import语句需要使用大括号。

export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能对应一个方法。

本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。

// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
// 等同于
// export default add;

// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';

export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。

// 正确
export const a = 1;
// 正确
const b = 2;
export default b;
// 错误
export default const c = 3;

上面代码中,export default a的含义是将变量a的值赋给变量default。所以,最后一种写法会报错。

如果想在一条import语句中,同时输入默认方法和其他变量,可以写成下面这样。

import _, { each } from 'lodash';
export default function (obj) {
  // ···
}
export function each(obj, iterator, context) {
  // ···
}
export { each as forEach };

如果要输出默认的值,只需将值跟在export default之后即可。

export default 42;

export default也可以用来输出类。

// MyClass.js
export default class { ... }

// main.js
import MyClass from 'MyClass';
let o = new MyClass();

浏览器加载规则:

1、浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。

浏览器对于带有type="module"的<script>,都是异步加载,不会造成堵塞浏览器,

<script type="module" src="./foo.js"></script>

2.ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。

<script type="module">
  import utils from "./utils.js";

  // other code
</script>

AMD

AMD即 Asynchronous Module Definition,中文名是“异步模块定义”的意思。它是一个在浏览器端模块化开发的规范,AMD 是 RequireJS 在推广过程中对模块定义的规范化产出,所以AMD规范的实现,就是的require.js了

特点 :异步加载,不阻塞页面的加载,能并行加载多个模块,但是不能按需加载,必须提前加载所需依赖

参考文章:

Javascript模块化编程(一):模块的写法
CommonJS规范 - 《JavaScript 标准参考教程(alpha)》
6模块更多用法及知识

这是一个使用typecho的博客,以后有时间的话可能使用node搭建后端重新做起,或者学习一点java做起。学习之路慢慢...