Node 开发命令行工具指南

前段日子为了工作上的方便, 我给自家公司的项目写了一个用于读写数据的 CLI 工具 shimo-cli. 这个项目可以用来方便的批量清理测试数据, 将线上的内容转换到本地开发环境中调试等等.

不同于 web 应用, 基于命令行(CLI)的应用与使用者的交互主要来源于用户终端的输入与输出. 在 nodejs 中主要是依靠 streamreadline 模块以及从 process.stdin / process.stdout 拿到的输入输出流来进行与终端的交互的.

常用库

在讨论具体的实现方式前, 我要先介绍一些很常用的好用的库:

Inquirer.js

Inquirer.js

这个库封装了许多常用的 CLI 交互方式, 例如单选多选, 输入等. 基本上可以满足绝大多数 CLI 应用的需要. 然而 shimo-cli 并没有直接用到 Inquirer, 不过参考了很多其中实现.

meow

meow 也是一个很常用的库. 他定义了一套 CLI 参数声明/解析规范. 并对外提供了 --help 方法.

chalk

chalk 用于对输出流的内容进行样式处理, 例如颜色,背景色,加粗等.

string-width

string-width可以获得文字的宽度, 并对 utf-8 字符与 emoji 做了特殊处理.

cli-width

cli-width 用来获得终端的宽度. 通常我们可以通过 process.stdout.columns 拿到终端宽度, 但是 cli-width 库中对许多奇奇怪怪的终端做了兼容性处理.

ink

ink 是一个用 React 组件编写 cli 应用的库. 类似于 ReactNative, ink 也是基于 yoga 处理布局的.

常见问题

如何接收用户输入

shimo-cli 的第一步就是完成登录鉴权,需要用户输入账号密码.

通过 process.stdin.resume 可以用来开启输入流的监听.

1
2
3
process.stdin.resume().on("data", chunk => {
process.stdin.pause();
});

对于密码的输入, 通常我们不期望回显在屏幕上, 通过 process.stdin.setRawMode(true) 可以避免输入的内容直接显示, 但对输入的接收也变成了原生模式, 需要手动检测用户输入的回车与中断符(Command + C)

Demo

按键交互

上述的 rawMode 可以捕获用户的原始输入(回车,方向键等). 利用这一点, 可以实现基于命令行的”加载更多”效果.

shimo-cli 会从 web 上获取含分页的列表, 我们期望每次显示一页的数据, 当用户输入回车时加载下一页的数据.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
async function more() {
process.stdout.write("-- Press Enter --\n");
return new Promise<boolean>(resolve => {
process.stdin.setRawMode!(true);
process.stdin.resume().on("data", (result: Buffer) => {
const char = result.toString("utf8");
switch (char) {
case "\n":
case "\r":
case "\u0004": {
process.stdin.setRawMode!(false);
process.stdin.pause();
clearLine();
resolve(true);
break;
}
case "\u0003": {
process.stdin.setRawMode!(false);
process.stdin.pause();
process.stdout.write("\n^C");
process.exit();
}
}
});
});
}

另外, 为了提示用户,每次数据显示完成后会显示 -- Press Enter --\n, 提示用户的交互行为. 出于洁癖的考虑, 当展示下一页的数据时, 希望数据之间可以连续, 因此我们需要将 -- Press Enter --\n 删除.

nodejs 提供了 readline 模块进行对终端的显示及光标的控制.

1
2
3
4
5
const readline = require("readline");
function clearLine() {
readline.moveCursor(process.stdout, 0, -1);
readline.clearLine(process.stdout, 0);
}

Demo

调用外部编辑器

调用外部编辑器可以直接使用 external-editor 库.

实现的方式也比较简单, 通过 childProcess 新建一个编辑器进程并在临时目录下创建一个文件用来记录编辑内容, 通过子进程的退出事件判断为输入的结束.

一般的编辑器可以根据后缀进行不同的高亮提示, 利用这点在创建文件时设置对应的后缀即可实现文本高亮.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async function editFile(filePath: string) {
return new Promise<void>(resolve => {
const editor = process.env.EDITOR || "vi";
const child = spawn(editor, [filePath], {
stdio: "inherit"
});
child.on("exit", () => {
resolve();
});
});
}

async function getContentFromEditor(
initContent: string = "",
suffix: string = "js"
) {
const editTempFile = getOneTmpFilePath(suffix);
fs.writeFileSync(editTempFile, initContent);
await editFile(editTempFile);
const content = fs.readFileSync(editTempFile).toString();
fs.unlinkSync(editTempFile);
return content;
}

Demo

流式调用

在 Unix 下多个程序通常以管道的方式配合使用

1
git blame foo | grep bar

在 node 中管道的输入与用户输入一样, 直接使用 readStream.resume 即可开始监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async function read() {
return new Promise((resolve, reject) => {
const clear = setTimeout(() => {
process.stdin.pause();
reject("timeout");
}, 500);
process.stdin.resume();
let data = "";
process.stdin.on("data", chunk => {
clearTimeout(clear);
data += chunk;
});
process.stdin.on("end", () => {
process.stdin.pause();
resolve(data.trim());
});
process.stdin.on("error", err => {
reject(err);
});
});
}

Demo

[Electron]Redux 主从模式

在 Redux 上实现主从听起来是一件很奇怪的事情, 事实上我也只有最近折腾 Electron 项目时才用上了这么奇怪的技巧.

先描述下项目的业务场景: 我们的项目简单的说是一个基于 Electron 的文件管理系统, 类似于 Google Driver, 主要功能有如下几点:

  1. 支持树形目录结构的文件系统
  2. 多标签页同时打开
  3. 本地的数据存储与异步同步

由于 Electron 的版本是在 web 的基础上增强了本地数据存储的特性, 很自然的, 我们的页面复用了 web 版 React + Redux 的架构. 并在 Redux 的 Action 层封装了从本地获取数据同时更新远程资源的逻辑, 并以中间件的形式实现了本地数据到服务端的增量同步.

对 web 而言, 每打开一个页面就意味一个 Redux 实例被创建, 对应在 Electron 上也就是每一个 Webcontent 对应了一个 Redux. 这带来了许多问题

  1. 不同的 Redux 数据内容可能不相同

    比如在 A 进程编辑了文件名, B 进程看到的也应该是编辑后的文件名, 无论是否联网

  2. 数据同步存在竞争关系

    这里的同步是双向的, 可能一个文件创建的动作被两个不同的进程推送给了服务端, 也可能是两个不同的进程同时操作了同一个本地数据, 导致本地出现了预期外的结果.

  3. 存在一定的网络请求浪费

    服务端到本地的同步是通过 socket 实现的, 这意味着每一个进程都需要建立一个 socket 连接.
    并且当一个新的进程被创建时, 所有初始化的请求都会被触发一遍

根据上面的问题, 解答的方法几乎是不言而喻的. 如果可以有一个 ReduxMaster 来负责内容同步与网络请求, 在所有需要用到 Redux 的进程创建一个 ReduxClient, 只用来同步 ReduxMaster 的状态, 所有的问题就都迎刃而解了.

(PS: 其实一开始我是叫 Master / Slaver 的, 这样的命名更加传统, 不过有一点点不符合现代人的政治正确标准)

原理

先抽象出一个简单的例子, 文件重命名. 在 Redux 中, 我们需要 dispatch 一个 updateFileName 的 Action, 经过 file 的 reducer, 将最终的状态存入 store.

现在, 这个事件由某一个 ReduxClient 触发, 我们希望由 ReduxMaster 来实现这个 Action, 再将状态更新至每一个 ReduxClient 中.

在 ReduxClient 中, 我们需要一个 Redux 中间件 将需要由 ReduxMaster 执行的 action 拦截并将 action 的名称与参数通知给 ReduxMaster 所在的进程. 再由 ReduxMaster 将执行完的结果广播给出去. 最后由 ReduxClient 将结果转化为 state.

当然在现实中, Action 是会承载 sideEffect 的. 以上面的例子而言 updateFileName 内部可能包含了一个网络请求, 这样的 Action 无法直接在进程间传递. 因此我们需要在各个进程间做出一个约定, 将 Action 映射为一个字符串 ActionName. 这样, 进程通信的过程中, 我们只需要传递 ActionName 和参数, 就可以由 ReduxMaster 完成一模一样的请求.

这里涉及到的不同的进程之间的通信, 在 Electron 中, 用 ipc 可以很容易的达成. 在我们的例子里, ReduxMaster 运行在一个 renderer 进程中, ReduxClient 通过获取 Master 所在的 Webcontents.send 发起通知, ReduxMaster 与 ReduxClient 均通过 ipcrenderer.on 接收通知. ReduxMaster 通过遍历所有 webContents 的 send 方法进行广播.

如果想在主进程里运行 ReduxMaster, 把 Webcontents.send 替换为 ipcrenderer.send, 接收替换为 ipcMain.on 即可.

Read More

[笔记]electron 踩到的坑

file 协议缺失 content-type

通过 electron.protocol.registerFileProtocolelectron.protocol.interceptFileProtocol 产生的协议, Response Header 中是不包含 content-type 的. 这会导致在某些奇怪的问题, 比如注册的 serviceworker 无法被识别等.

解决方法是使用 registerBufferProtocol 代替 registerFileProtocol 协议.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interceptBufferProtocol('file', (request, callback) => {
const filePath = getFilePathFromRequest(request)
fs.readFile(filePath, (err, data) => {
if (err) {
// report error
callback()
} else {
callback({
// 通过开源库 mime-types 获得后缀对应的 mime type
mimeType: getMimeType(filePath),
data
})
}
})
})

上面的例子可以看出来 buffer protocol 完美的替代了 file protocol

clearStorageData 清理 localStorage 失败

通过 session.defaultSession.clearStorageData 清理 file:// 下的 localStorage 会失败, 必须强制指定一个 origin

1
2
3
4
session.defaultSession.clearStorageData({
origin: process.env.APP_PROTOCOL,
storages: ['localstorage']
}, callbackFn)

给 electron 提了一个 issue, 有开发者答复说是 chromium 的 bug, 会在升级到 chromium66的内核后修复. 所以理论上 electron 3+ 的版本不再会有这个问题, 待验证

Electron 对 io 操作有一定的优化, setCookie 并不是立即作用于磁盘上.

推荐在 before-quit 的时候调用一次 cookies.flushStore

代理导致 electron 应用崩溃

我所遇到过 Electron 的问题中最费解的一个, 至今不理解这个问题原理是什么. 只知道当系统中设置了 pac 方式的代理时, 网络状态的切换会导致程序崩溃.

我的解决方案是, 在 electron 中通过 session.setProxy 的方式强制设置一个代理(比如设置所有请求走 DIRECT). 就可以防止程序崩溃.

文件关联

electron-builder 提供了 fileAssociations 配置项可以在安装时注册文件的启动方式.

在 mac 系统中, 我们可以通过 electron.app.on(‘open-file’) 事件监听打开文件的行为.

由于打开文件的行为可以触发客户端的启动, open-file 事件的注册必须尽可能的早(在 app.ready 之前), 并在 app.ready 后触发对应的操作.

windows 与 linux 下, 可以在 process.env.argv 中获得打开的文件

在 electron 中使用 Chrome 扩展

官方文档里有一篇指南: https://electronjs.org/docs/tutorial/devtools-extension 但比较不易被人发现.

react-dev-tool 举例

1
BrowserWindow.addDevToolsExtension('/Users/whoami/Library/Application Support/Google/Chrome/Default/Extensions/fmkadmapgofadopljbjfkapdkoienihi/3.3.1_0/')

扩展的版本名会随着 Chrome 扩展升级变化.

Electron 2 -> 3 的坑

抛开官方弃用的 Api 外, 还有不少由于 chromium 升级造成的问题:

1. webview-tag 的 html 事件不再对外冒泡

解决方法是通过 ipc 手动触发一个外部的对应事件

https://github.com/electron/electron/issues/14258#issuecomment-416794070

2. webview-tag 中通过菜单项的复制粘贴很多场合不正常

主要是 windows 的场合, 部分菜单功能在 windows, macos 下均不可用 (比如 copyWithStyle)

解决方法是不要用菜单的 role 属性, 而是通过菜单项手动触发 webcontent 的对应指令

https://github.com/electron/electron/issues/15219

3. clearStorageData 在 file:// 下无法删除 indexdb

甚至在 chrome-dev-tool 下清除也是无效的.

目前尚未找到完美的替代方法, 只能通过渲染进程中 indexedDB.deleteDatabase 逐个删除.

[笔记]jest 使用体验

最近在公司的项目里引入了 jest, 花了一段时间补充了各种用例, 由于是第一次正式使用 jest, 也遇到了一些问题, 写一篇笔记记录一下.

snapshot

对于大多数组件, jest 提供的 snapshot 都是一种很便利的工具. 只要能确定生成快照时的状态时正确的, 那么就可以比较轻易的发现修改扩大了影响范围的场景.

因此, 我们想要的结果自然是当一个组件没有被改变, 对应的 snapshot 每次生成也不会改变. 然而有一些因素可能会导致每次生成的snapshot 都不一样, 我们就遇到了两种情形:

相对时间

组件中可能会有与当前时间相关的逻辑, 比如今天生成的 snapshot 里, 时间的描述是 “1天前”, 那等到明天重新生成快照时, 时间就会变成了 “两天前”.

为此, 我们引入了 mockdate 的模块, 它会复写 window.Date, 使当前时间固定在某一时刻.

styled-components

styled-components 生成的元素, 在生成快照时会出现一个随机字符串的 className, 这个 className 会随着样式改变而变化. 然而我们是没有办法区分随机字符串的变化是否是我们期待的.

所幸 styled-components 提供了 jest-styled-components 来解决这个问题. 它会把生成的 css 也放在快照里, 这样是否是期待的变化就一目了然了.

Read More

[笔记]js-to-ts

前言

最近在把公司的一个 React + Webpack 的项目从 js 迁移到 typescript 上. 这里记录下迁移的过程以及遇到的一些问题.

在开始之前, 先安利一下 Typescript 官方文档提供的从 JavaScript 迁移, 以及其中提到的 React 项目转换指南. 这两篇文章也涵盖了我们迁移过程中相当大一部分内容.

配置文件相关

引用方式

tsc 默认对于 commonjs 的模块是这样加载的

1
import * as React from 'react'

我们不想改变原先的写法: import React from 'react'. 幸好, ts 提供了一个编译参数 allowSyntheticDefaultImports.

1
2
3
4
5
{
"allowSyntheticDefaultImports": true,
"module": "esnext",
"moduleResolution": "node"
}

相对路径

我们项目的 webpack 配置将 src 目录配置在了 resolve 中

1
2
3
4
5
{
resolve: {
modules: ['node_modules', 'src']
}
}

代码中, 可以通过 `import ‘App’ from ‘components/App’ 的方式引用 src/components/App.jsx 文件

在 typescript 中, 需要配置 baseUrlpaths 来告诉 tsc 寻找文件的路径.

1
"baseUrl": "./src",

或者

1
2
3
4
5
6
7
8
9
10
11
{
compilerOptions: {
"baseUrl": ".",
"paths": {
"*": [
"src/*",
"node_modules/*"
]
}
}
}

二者略有不同. 从结论上看 paths 的方式会检测加载模块关联的声明, 对于声明缺失的模块编译时会抛出一个错误.

全局参数

对于项目中用到的全局变量, 以及 WebpackDefinePlugin 之类输出的东西, 我们可以建一个 src/global.d.ts 文件来声明进行声明, 比如:

1
declare const global: any

另一方面, webpack 通过X-loader 加载 js 之外的文件加载. 这些东西 tsc 看不懂, 就会报错. 我们希望 tsc 在 type checking 时能跳过这些模块, 也可以在 global.d.ts 里加上模块的声明:

1
2
3
4
5
declare module '*.png'
declare module '*.jpg'
declare module '*.svg'
declare module '*.mp4'
// ...

顺带一提, 我们的 WebpackDefinePlugin 会输出一个 process 变量来同步一部分配置文件到前端. 一开始我是拒绝在面向 browser 的项目引用 @types/node , 而是通过 global.d.ts 引入的. 然而很多第三方的模块会带入 @types/node 造成重复声明的异常, 最后只好直接显式的引入 @types/node 了.

从 js 到 ts(x)

tsc 的 allowJS 参数支持对 js 的解析. 但是当将 js 后缀改为 ts(x)时, 通常会出现大量的问题, 这里总结下在我们项目中比例较大的一部分情况.

是否包含 jsx

过去, 我们并没有使用 .jsx 的后缀, 无论是否包含 JSX 格式的数据均保存为 .js 文件. 然而在 .ts 的文件中使用 JSX 会导致一个错误.

因此改后缀之前需要判断 文件中是否包含 JSX 格式的数据, 分别转换为对应的 .ts 或 .tsx 文件

Read More

[笔记]使用 sentry

sentry 介绍

sentry 是一个很流行的错误监控工具. 它能很好的监听线上异常以及开发者手动抛出的错误并记录.

除了官方提供的 sentry.io 外, 也支持自己部署 (Sentry 简易搭建指南), 适用于安全性有所要求的项目.

sentry 的界面看起来十分高大上. Issues 界面可以看到所有网站中抛出的异常(及开发者想记录的信息) 相同内容的记录会合并在一起并记录触发次数, 避免被某一个问题刷屏的情况. 也可以像 GitHub 的 issue 一样 assign 给相应的开发者处理. 并且 sentry 也支持邮寄和各种 IM 的 robot, 十分好用.

release

代码的不同版本, 会对应不同的 bug, sentry 中用 release 来标识一个代码包的当前版本. 理论上不同代码包应该对应一个唯一性的 release version

价格

自搭不谈, sentry.io 的服务我觉得还是很亲民的, 免费用户也提供了每月 10,000 次的 events 记录, 不过免费用户不提供多人共享一个项目, 并且events 的保存时间也比较有限.

sentry 配置

想使用 sentry 首先要在 sentry.io (或是自建服务)上注册一个项目. 项目注册后会得到一个类似下面这样的 url

1
https://abcdefghijklmn@sentry.io/12345

用来作为监控的来源识别

参考官方文档, 使用 sentry 需要引入其 sdk 脚本文件 ravenjs.

可以通过静态脚本引入

1
<script src="https://cdn.ravenjs.com/3.19.1/raven.min.js" crossorigin="anonymous"></script>

也可以用 npm 等包管理软件引入 npm install raven-js

加载 raven 后, 配置和之前注册项目的关联

Read More

[笔记]electron 实践笔记

最近工作上需要把 web 上的内容打包成一个客户端, 以便于提供一些更好用户体验的功能. 我们的前端是目前比较标准的 webpack + react. 因此, 首先想到的是把 webpack 生成的文件丢进 electron 里运行.

看似很简单, 但实践还是踩了蛮多坑的. 当然一定程度上也是由于我个人对 electron 并不太熟悉, 各种实践也是一边摸索一边进行的…

工作原理

electron 启动时会执行一个 js 脚本创建一个 node 进程.

1
electron ./main.js

这个进程可以用于初始化 electron 应用. 并且控制窗口的打开与事件监听. 在官方文档里, 这个进程被称作 Main Process

1
2
3
4
5
6
7
8
9
10
const electron = require('electron')

const { BrowserWindow } = electron

mainWindow = new BrowserWindow({width: 1280, height: 720})

// Load code/index.html page
mainWindow.loadURL('file://index.html')

// ...

通过 loadURL 打开的窗口, 基本上就是一个内嵌 chrome 内核的浏览器, 只不过 electron 还贴心的注入了一些 nodejs 的模块, 比如 commonjs 的 require/modules, fs, path 之类的.

这个浏览器窗口的进程在官方文档里, 被称作 Renderer Process.

主进程和渲染进程可以调用的 API 各不相同, 渲染进程里提供了一个 remote 的模块可以用来执行主模块里的东西.

通过这些接口, 我们可以做到许多浏览器做不到的事情, 比如本地文件的读写, 代理, cors, 设备驱动, 等等等等…

Read More

[笔记]WebRTC 学习 FAQ

Q: WebRTC 是什么, 能做什么, 社区评价?

https://www.zhihu.com/question/22301898

1
WebRTC 全称 Web Real-Time Communication。它并不是单一的协议, 包含了媒体、加密、传输层等在内的多个协议标准以及一套基于 JavaScript 的 API。通过简单易用的 JavaScript API ,在不安装任何插件的情况下,让浏览器拥有了 P2P音视频和数据分享的能力。

Q: WebRTC 技术栈

Q: 如何搭建一个 WebRTC sample?

https://webrtc.github.io/samples/

Q: WebRTC有哪些实现?

https://github.com/rainzhaojy/blogs/issues/3

  1. Browser Support
  2. WebRTC native code (https://webrtc.org/native-code/)

Q: WebRTC 如何进行网络请求?

通过 P2P 协议

https://bloggeek.me/4-p2p-webrtc-facts/

  1. WebRTC 在 p2p 上和 voIp 一样
    这意味着从一端到另外一端是 “best effort” 的机制
  2. WebRTC 是唯一的浏览器 p2p 方案
  3. P2P 还是需要 STUN and TURN
  4. WebRTC 还是需要外部信号协议

Read More

[笔记]Apollo 初体验

趁着 GitHub 的 Api 转向 GraphQL 时, 了解了下相关知识和其中一个蛮有名气的实现: Apollo.

先简单的唠叨两句, 相对比 Rest 和 GraphQL, 我觉得后者比较适合 Full-stack 框架的设计. 前端定义好的 SQL 可以完美对应 FE 的 model 层以及生成 PropTypes 或是 TypeScript 的类型定义. 而且 web app 接口的变化大多时候是前端驱动的, GraphQL 的特点之一, 业务带来的字段与查询变化, 开发人员可以优先专注前端, generate 出相应的接口与数据模型.

发布时, 如果前后端的规则一致, 也可以对两端同时编译, 将前端 GraphQL 的细节抽象, 转换为类似 Rest 的接口形式. 避免了请求包含字段细节从而增加安全性, 以及不用去烦恼前后端通讯的 graphQL 大小.

说到底, 想要发挥 GraphQL 的优势, 需要一套能 carry 全场的 graphql-generator 工具, 然而凭借我(两三天)对 graphql 的了解, 目前尚没有发现特别通用的工具.

Read More

webpack 介绍

模块化的前身今世

近年随着硬件升级以及网络普及, 互联网应用的重心慢慢向富客户端偏移. 前端开发与构建也开始遇到了和其他语言相似的挑战: 代码复用, 关注分离以及灵活的继承.

社区的主流趋势是在 JavaScript 中引入模块化的概念以及一个模块加载系统. 目前主流的模块管理有 AMD, CommonJs 以及 ES6 规范的 import

由于浏览器加载 script 异步的天性, 最先(也是最自然)诞生的模块是 异步模块定义(Asynchronous Module Definition), 简称 AMD.

1
define(['myDep', 'jquery'], function(myDep, $) {...})

AMD 通过一个数组声明自己所依赖的文件,当所有依赖加载完毕后, 作为参数传递给回调函数. RequireJs 是一个 AMD 比较常见的实现. 类似的还有国人玉伯写的 Sea.js(虽然 Sea.js 提出了 CMD 的概念, 但本质还是 AMD 的变形).

AMD 允许以 runtime 的形式运行, 动态的加载依赖文件, 也可以一次性将依赖构建在一个或多个文件中运行, 后者在生产环境下使用较为普遍, 可以有效的优化页面加载速度, 例如 Require.js 提供的 r.js, 在 CLI 中执行可以将源码构建并优化成生产环境运行的目标代码, 写入指定目录, 这一点已经和 webpack 比较相似了.

另外值得一提的是 Require.js 并不仅仅可以处理 js 模块, 还可以用于引入 css, 文本资源等等. 通过一些 Require.js 的插件来实现, 在代码中也需要显示的声明文件类型 (eg: require('css!style.css')). 相比日后 webpack 要繁琐些.

CommonJs 是 nodejs 默认的加载方式:

1
2
const $ = require(‘jQuery’);
console.log($.version);

这种同步的写法在浏览器中显然无法直接运行. 但我们可以在构建阶段将 js 文本解析成抽象语法树(Abstract Syntax Tree), 然后将所有 require 的内容合并在一个文件中. 这样的解析工具有 Browserify 以及后来的 Webpack.

WebpackBrowserify 基础上做了一些优化, 比如可以将构建的文件拆分成多个 chunk, 不用刷新页面就可以’热替换’模块等.

ES6 的 import 实际上和 CommonJs 比较相似, 不单独赘述.

AMD 需要把真正的代码放入一个回调函数中, 很多类库在文件头部会出现判断是否存在 define 和 define.amd 的判断逻辑然后分别调以不同的模块加载方式. 相较之下, CommonJsES6 import 的用法对于源码破坏性要小的多, 也更符合其他语言的模块加载逻辑.

Read More