本文首发于 vivo互联网技术 微信公众号 
链接:https://mp.weixin.qq.com/s/15sedEuUVTsgyUm1lswrKA
作者:Morrain

一、前言

上一篇《前端科普系列(2):Node.js 换个角度看世界》,我们聊了 Node.js 相关的东西,Node.js 能在诞生后火到如此一塌糊涂,离不开它成熟的模块化实现,Node.js 的模块化是在 CommonJS 规范的基础上实现的。那 CommonJS 又是什么呢?

先来看下,它在维基百科上的定义:

CommonJS 是一个项目,其目标是为 JavaScript 在网页浏览器之外创建模块约定。创建这个项目的主要原因是当时缺乏普遍可接受形式的 JavaScript 脚本模块单元,模块在与运行JavaScript 脚本的常规网页浏览器所提供的不同的环境下可以重复使用。

我们知道,很长一段时间 JavaScript 语言是没有模块化的概念的,直到 Node.js 的诞生,把 JavaScript 语言带到服务端后,面对文件系统、网络、操作系统等等复杂的业务场景,模块化就变得不可或缺。于是 Node.js 和 CommonJS 规范就相得益彰、相映成辉,共同走入开发者的视线。

由此可见,CommonJS 最初是服务于服务端的,所以我说 CommonJS 不是前端,但它的载体是前端语言 JavaScript,为后面前端模块化的盛行产生了深远的影响,奠定了结实的基础。CommonJS:不是前端却革命了前端!

二、为什么需要模块化

1、没有模块化时,前端是什么样子

在之前的《Web:一路前行一路忘川》中,我们提到过 JavaScript 诞生之初只是作为一个脚本语言来使用,做一些简单的表单校验等等。所以代码量很少,最开始都是直接写到 <script> 标签里,如下所示:

// index.html
<script>
var name = 'morrain'
var age = 18
</script>

// index.html
<script src="./mine.js"></script> // mine.js
var name = 'morrain'
var age = 18

再后来,更多的开发者参与进来,更多的 js 文件被引入进来:

// index.html
<script src="./mine.js"></script>
<script src="./a.js"></script>
<script src="./b.js"></script> // mine.js
var name = 'morrain'
var age = 18 // a.js
var name = 'lilei'
var age = 15 // b.js
var name = 'hanmeimei'
var age = 13

2、模块化的原型

为了解决全局变量污染的问题,开发者开始使用命名空间的方法,既然命名会冲突,那就加上命名空间呗,如下所示:

// index.html
<script src="./mine.js"></script>
<script src="./a.js"></script>
<script src="./b.js"></script> // mine.js
app.mine = {}
app.mine.name = 'morrain'
app.mine.age = 18 // a.js
app.moduleA = {}
app.moduleA.name = 'lilei'
app.moduleA.age = 15 // b.js
app.moduleB = {}
app.moduleB.name = 'hanmeimei'
app.moduleB.age = 13
app.moduleA.name 来取到模块A中的名字,但是也可以通过 app.moduleA.name = 'rename' 来任意改掉模块A中的名字,而这件事情,模块A却毫不知情!这显然是不被允许的。

聪明的开发者又开始利用 JavaScript 语言的函数作用域,使用闭包的特性来解决上面的这一问题。

// index.html
<script src="./mine.js"></script>
<script src="./a.js"></script>
<script src="./b.js"></script> // mine.js
app.mine = (function(){
var name = 'morrain'
var age = 18
return {
getName: function(){
return name
}
}
})() // a.js
app.moduleA = (function(){
var name = 'lilei'
var age = 15
return {
getName: function(){
return name
}
}
})() // b.js
app.moduleB = (function(){
var name = 'hanmeimei'
var age = 13
return {
getName: function(){
return name
}
}
})()

现在 b.js 模块可以通过 app.moduleA.getName() 来取到模块A的名字,但是各个模块的名字都保存在各自的函数内部,没有办法被其它模块更改。这样的设计,已经有了模块化的影子,每个模块内部维护私有的东西,开放接口给其它模块使用,但依然不够优雅,不够完美。譬如上例中,模块B可以取到模块A的东西,但模块A却取不到模块B的,因为上面这三个模块加载有先后顺序,互相依赖。当一个前端应用业务规模足够大后,这种依赖关系又变得异常难以维护。

综上所述,前端需要模块化,并且模块化不光要处理全局变量污染、数据保护的问题,还要很好的解决模块之间依赖关系的维护。

三、CommonJS 规范简介

既然 JavaScript 需要模块化来解决上面的问题,那就需要制定模块化的规范,CommonJS 就是解决上面问题的模块化规范,规范就是规范,没有为什么,就和编程语言的语法一样。我们一起来看看。

1、CommonJS 概述

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

// a.js
var name = 'morrain'
var age = 18

CommonJS 规范还规定,每个模块内部有两个变量可以使用,require 和 module。

require 用来加载某个模块

module 代表当前模块,是一个对象,保存了当前模块的信息。exports 是 module 上的一个属性,保存了当前模块要导出的接口或者变量,使用 require 加载的某个模块获取到的值就是那个模块使用 exports 导出的值

// a.js
var name = 'morrain'
var age = 18
module.exports.name = name
module.exports.getAge = function(){
return age
} //b.js
var a = require('a.js')
console.log(a.name) // 'morrain'
console.log(a.getAge())// 18

2、CommonJS 之 exports

为了方便,Node.js 在实现 CommonJS 规范时,为每个模块提供一个 exports的私有变量,指向 module.exports。你可以理解为 Node.js 在每个模块开始的地方,添加了如下这行代码。

var exports = module.exports

于是上面的代码也可以这样写:

// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.getAge = function(){
return age
}

有一点要尤其注意,exports 是模块内的私有局部变量,它只是指向了 module.exports,所以直接对 exports 赋值是无效的,这样只是让 exports 不再指向module.exports了而已。

如下所示:

// a.js
var name = 'morrain'
var age = 18
exports = name

如果一个模块的对外接口,就是一个单一的值,可以使用 module.exports 导出

// a.js
var name = 'morrain'
var age = 18
module.exports = name

3、CommonJS 之 require

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

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

// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.getAge = function(){
return age
}
// b.js
var a = require('a.js')
console.log(a.name) // 'morrain'
a.name = 'rename'
var b = require('a.js')
console.log(b.name) // 'rename'

如上所示,第二次 require 模块A时,并没有重新加载并执行模块A。而是直接返回了第一次 require 时的结果,也就是模块A的 module.exports。

还一点需要注意,CommonJS 模块的加载机制是,require 的是被导出的值的拷贝。也就是说,一旦导出一个值,模块内部的变化就影响不到这个值 。

// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.age = age
exports.setAge = function(a){
age = a
}
// b.js
var a = require('a.js')
console.log(a.age) // 18
a.setAge(19)
console.log(a.age) // 18

四、CommonJS 实现

了解 CommonJS 的规范后,不难发现我们在写符合 CommonJS 规范的模块时,无外乎就是使用了 require 、 exports 、 module 三个东西,然后一个 js 文件就是一个模块。如下所示:

// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.getAge = function () {
return age
}
// b.js
var a = require('a.js')
console.log('a.name=', a.name)
console.log('a.age=', a.getAge()) var name = 'lilei'
var age = 15
exports.name = name
exports.getAge = function () {
return age
}
// index.js
var b = require('b.js')
console.log('b.name=',b.name)

如果我们向一个立即执行函数提供 require 、 exports 、 module 三个参数,模块代码放在这个立即执行函数里面。模块的导出值放在 module.exports 中,这样就实现了模块的加载。如下所示:

(function(module, exports, require) {
// b.js
var a = require("a.js")
console.log('a.name=', a.name)
console.log('a.age=', a.getAge()) var name = 'lilei'
var age = 15
exports.name = name
exports.getAge = function () {
return age
} })(module, module.exports, require)

我们以 webpack 为例,看看如何实现对 CommonJS 规范的支持。我们使用 webpack 构建时,把各个模块的文件内容按照如下格式打包到一个 js 文件中,因为它是一个立即执行的匿名函数,所以可以在浏览器直接运行。

// bundle.js
(function (modules) {
// 模块管理的实现
})({
'a.js': function (module, exports, require) {
// a.js 文件内容
},
'b.js': function (module, exports, require) {
// b.js 文件内容
},
'index.js': function (module, exports, require) {
// index.js 文件内容
}
})

接下来,我们需要按照 CommonJS 的规范,去实现模块管理的内容。首先我们知道,CommonJS 规范有说明,加载过的模块会被缓存,所以需要一个对象来缓存已经加载过的模块,然后需要一个 require 函数来加载模块,在加载时要生成一个 module,并且 module 上 要有一个 exports 属性,用来接收模块导出的内容。

// bundle.js
(function (modules) {
// 模块管理的实现
var installedModules = {}
/**
* 加载模块的业务逻辑实现
* @param {String} moduleName 要加载的模块名
*/
var require = function (moduleName) { // 如果已经加载过,就直接返回
if (installedModules[moduleName]) return installedModules[moduleName].exports // 如果没有加载,就生成一个 module,并放到 installedModules
var module = installedModules[moduleName] = {
moduleName: moduleName,
exports: {}
} // 执行要加载的模块
modules[moduleName].call(modules.exports, module, module.exports, require) return module.exports
} return require('index.js')
})({
'a.js': function (module, exports, require) {
// a.js 文件内容
},
'b.js': function (module, exports, require) {
// b.js 文件内容
},
'index.js': function (module, exports, require) {
// index.js 文件内容
}
})

五、其它前端模块化的方案

我们对 CommonJS 的规范已经非常熟悉了,require 命令的基本功能是,读入并执行一个 js 文件,然后返回该模块的 exports 对象,这在服务端是可行的,因为服务端加载并执行一个文件的时间消费是可以忽略的,模块的加载是运行时同步加载的,require 命令执行完后,文件就执行完了,并且成功拿到了模块导出的值。

这种规范天生就不适用于浏览器,因为它是同步的。可想而知,浏览器端每加载一个文件,要发网络请求去取,如果网速慢,就非常耗时,浏览器就要一直等 require 返回,就会一直卡在那里,阻塞后面代码的执行,从而阻塞页面渲染,使得页面出现假死状态。

为了解决这个问题,后面发展起来了众多的前端模块化规范,包括 CommonJS 大致有如下几种:

1、AMD (Asynchronous Module Definition)

在聊 AMD 之前,先熟悉一下 RequireJS。

官网是这么介绍它的:

"RequireJS is a JavaScript file and module loader. It is optimized for in-browser use, but it can be used in other JavaScript environments, like Rhino and Node. Using a modular script loader like RequireJS will improve the speed and quality of your code."

翻译过来大致就是:

RequireJS 是一个 js 文件和模块加载器。它非常适合在浏览器中使用,但它也可以用在其他 js 环境, 就像 Rhino 和 Node。使用 RequireJS 加载模块化脚本能提高代码的加载速度和质量。

它解决了 CommonJS 规范不能用于浏览器端的问题,而 AMD 就是 RequireJS 在推广过程中对模块定义的规范化产出。

来看看 AMD 规范的实现:

<script src="require.js"></script>
<script src="a.js"></script>
define(id?, dependencies?, factory)

所以模块A可以这么定义:

// a.js
define(function(){
var name = 'morrain'
var age = 18
return {
name,
getAge: () => age
}
})
// b.js
define(['a.js'], function(a){
var name = 'lilei'
var age = 15
console.log(a.name) // 'morrain'
console.log(a.getAge()) // 18
return {
name,
getAge: () => age
}
})

RequireJS 的基本思想是,通过 define 方法,将代码定义为模块。当这个模块被 require 时,它开始加载它依赖的模块,当所有依赖的模块加载完成后,开始执行回调函数,返回值是该模块导出的值。AMD 是 "Asynchronous Module Definition" 的缩写,意思就是"异步模块定义"。

2、CMD (Common Module Definition)

和 AMD 类似,CMD 是 Sea.js 在推广过程中对模块定义的规范化产出。Sea.js 是阿里的玉伯写的。它的诞生在 RequireJS 之后,玉伯觉得 AMD 规范是异步的,模块的组织形式不够自然和直观。于是他在追求能像 CommonJS 那样的书写形式。于是就有了 CMD 。

Sea.js 官网这么介绍 Sea.js:

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

"简单友好的模块定义规范:Sea.js 遵循 CMD 规范,可以像 Node.js 一般书写模块代码。自然直观的代码组织方式:依赖的自动加载、配置的简洁清晰,可以让我们更多地享受编码的乐趣。"

来看看 CMD 规范的实现:

<script src="sea.js"></script>
<script src="a.js"></script>
// 所有模块都通过 define 来定义
define(function(require, exports, module) { // 通过 require 引入依赖
var a = require('xxx')
var b = require('yyy') // 通过 exports 对外提供接口
exports.doSomething = ... // 或者通过 module.exports 提供整个接口
module.exports = ... }) // a.js
define(function(require, exports, module){
var name = 'morrain'
var age = 18 exports.name = name
exports.getAge = () => age
})
// b.js
define(function(require, exports, module){
var name = 'lilei'
var age = 15
var a = require('a.js') console.log(a.name) // 'morrain'
console.log(a.getAge()) //18 exports.name = name
exports.getAge = () => age
})

Sea.js 可以像 CommonsJS 那样同步的形式书写模块代码的秘诀在于:当 b.js 模块被 require 时,b.js 加载后,Sea.js 会扫描 b.js 的代码,找到 require 这个关键字,提取所有的依赖项,然后加载,等到依赖的所有模块加载完成后,执行回调函数,此时再执行到 require('a.js') 这行代码时,a.js 已经加载好在内存中了

3、ES6 Module

前面提到的 CommonJS 是服务于服务端的,而 AMD、CMD 是服务于浏览器端的,但它们都有一个共同点:都在代码运行后才能确定导出的内容CommonJS 实现中可以看到。

还有一点需要注意,AMD 和 CMD 是社区的开发者们制定的模块加载方案,并不是语言层面的标准。从 ES6 开始,在语言标准的层面上,实现了模块化功能,而且实现得相当简单,完全可以取代 CommonJS 和 CMD、AMD 规范,成为浏览器和服务器通用的模块解决方案。

事实也是如些,早在2013年5月,Node.js 的包管理器 NPM 的作者 Isaac Z. Schlueter 说过 CommonJS 已经过时,Node.js 的内核开发者已经决定废弃该规范。原因主要有两个,一个是因为 Node.js 本身也不是完全采用 CommonJS 的规范,譬如在CommonJS 之 exports 中的提到 exports 属性就是 Node.js 自己加的,Node.js 当时是决定不再跟随 CommonJS 的发展而发展了。二来就是 Node.js 也在逐步用 ES6 Module 替代 CommonJS。

2017.9.12 Node.js 发布的 8.5.0 版本开始支持 ES6 Module。只不过是处于实验阶段。需要添加 --experimental-modules 参数。

2019.11.21 Node.js 发布的 13.2.0 版本中取消了 --experimental-modules 参数 ,也就是说从 v13.2 版本开始,Node.js 已经默认打开了 ES6 Module 的支持。

(1)ES6 Module 语法

任何模块化,都必须考虑的两个问题就是导入依赖和导出接口。ES6 Module 也是如此,模块功能主要由两个命令构成:export 和 import。export 命令用于导出模块的对外接口,import 命令用于导入其他模块导出的内容。

具体语法讲解请参考阮一峰老师的教程,示例如下:

// a.js
export const name = 'morrain'
const age = 18
export function getAge () {
return age
} //等价于
const name = 'morrain'
const age = 18
function getAge (){
return age
}
export {
name,
getAge
}
// b.js
import { name as aName, getAge } from 'a.js'
export const name = 'lilei'
console.log(aName) // 'morrain'
const age = getAge()
console.log(age) // 18 // 等价于
import * as a from 'a.js'
export const name = 'lilei'
console.log(a.name) // 'morrin'
const age = a.getAge()
console.log(age) // 18

从上面的例子可以看到,使用 import 命令的时候,用户需要知道所要导入的变量名,这有时候比较麻烦,于是 ES6 Module 规定了一种方便的用法,使用 export default命令,为模块指定默认输出。

// a.js
const name = 'morrain'
const age = 18
function getAge () {
return age
}
export default {
name,
getAge
} // b.js
import a from 'a.js'
console.log(a.name) // 'morrin'
const age = a.getAge()
console.log(age) // 18

显然,一个模块只能有一个默认输出,因此 export default 命令只能使用一次。同时可以看到,这时 import 命令后面,不需要再使用大括号了。

除了基础的语法外,还有 as 的用法、export 和 import 复合写法、export * from 'a'、import()动态加载 等内容,可以自行学习。

前面提到的 Node.js 已经默认支持 ES6 Module ,浏览器也已经全面支持 ES6 Module。至于 Node.js 和 浏览器 如何使用 ES6 Module,可以自行学习。

(2)ES6 Module 和 CommonJS 的区别

CommonJS 只能在运行时确定导出的接口,实际导出的就是一个对象。而 ES6 Module 的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及导入和导出的变量,也就是所谓的"编译时加载"。

正因为如此,import 命令具有提升效果,会提升到整个模块的头部,首先执行。下面的代码是合法的,因为 import 的执行早于 getAge 的调用。

// a.js
export const name = 'morrain'
const age = 18
export function getAge () {
return age
} // b.js
const age = getAge()
console.log(age) // 18
import { getAge } from 'a.js'

也正因为 ES6 Module 是编译时加载, 所以不能使用表达式和变量,因为这些是只有在运行时才能得到结果的语法结构。如下所示:

// 报错
import { 'n' + 'ame' } from 'a.js' // 报错
let module = 'a.js'
import { name } from module

前面在CommonJS 之 require有提到,require 的是被导出的值的拷贝。也就是说,一旦导出一个值,模块内部的变化就影响不到这个值。一起来看看,ES Module是什么样的。

先回顾一下之前的例子:

// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.age = age
exports.setAge = function(a){
age = a
} // b.js
var a = require('a.js')
console.log(a.age) // 18
a.setAge(19)
console.log(a.age) // 18

使用 ES6 Module 来实现这个例子:

// a.js
var name = 'morrain'
var age = 18
const setAge = a => age = a
export {
name,
age,
setAge
} // b.js
import * as a from 'a.js' console.log(a.age) // 18
a.setAge(19)
console.log(a.age) // 19

下一篇,聊聊 ES6+ 和 Babel,敬请期待……

六、参考文献

  1. CommonJS规范

  2. ES Module 的语法

  3. ES Module 的加载实现

  4. 前端模块化开发解决方案详解

  5. webpack模块化原理-commonjs

更多内容敬请关注 vivo 互联网技术 微信公众号

注:转载文章请先与微信号:Labs2020 联系。

前端科普系列(3):CommonJS 不是前端却革命了前端的更多相关文章

  1. 前端工程化系列[04]-Grunt构建工具的使用进阶

    在前端工程化系列[02]-Grunt构建工具的基本使用和前端工程化系列[03]-Grunt构建工具的运转机制这两篇文章中,我们对Grunt以及Grunt插件的使用已经有了初步的认识,并探讨了Grunt ...

  2. 前端监控系列4 | SDK 体积与性能优化实践

    背景 字节各类业务拥有众多用户群,作为字节前端性能监控 SDK,自身若存在性能问题,则会影响到数以亿计的真实用户的体验.所以此类 SDK 自身的性能在设计之初,就必须达到一个非常极致的水准. 与此同时 ...

  3. 前端工程化系列[06]-Yeoman脚手架核心机制

    在前端工程化系列[05] Yeoman脚手架使用入门这边文章中,对Yeoman的使用做了简单的入门介绍,这篇文章我们将接着探讨Yeoman这个脚手架工具内部的核心机制,主要包括以下内容 ❏ Yeoma ...

  4. 前端工程化系列[03]-Grunt构建工具的运转机制

    在前端工程化系列[02]-Grunt构建工具的基本使用这篇文章中,已经对Grunt做了简单的介绍,此外,我们还知道了该如何来安装Grunt环境,以及使用一些常见的插件了,这篇文章主要介绍Grunt的核 ...

  5. 前端安全系列(一):如何防止XSS攻击?

    原文:https://my.oschina.net/meituantech/blog/2218539 前端安全 随着互联网的高速发展,信息安全问题已经成为企业最为关注的焦点之一,而前端又是引发企业安全 ...

  6. 前端安全系列之二:如何防止CSRF攻击

    原文:https://my.oschina.net/meituantech/blog/2243958 背景 随着互联网的高速发展,信息安全问题已经成为企业最为关注的焦点之一,而前端又是引发企业安全问题 ...

  7. 前端安全系列之二:如何防止CSRF攻击?

    背景 随着互联网的高速发展,信息安全问题已经成为企业最为关注的焦点之一,而前端又是引发企业安全问题的高危据点.在移动互联网时代,前端人员除了传统的 XSS.CSRF 等安全问题之外,又时常遭遇网络劫持 ...

  8. 前端安全系列(二):如何防止CSRF攻击?

    前端安全系列(二):如何防止CSRF攻击?   背景 随着互联网的高速发展,信息安全问题已经成为企业最为关注的焦点之一,而前端又是引发企业安全问题的高危据点.在移动互联网时代,前端人员除了传统的 XS ...

  9. 2019前端面试系列——JS面试题

    判断 js 类型的方式 1. typeof 可以判断出'string','number','boolean','undefined','symbol' 但判断 typeof(null) 时值为 'ob ...

  10. 2019前端面试系列——Vue面试题

    Vue 双向绑定原理        mvvm 双向绑定,采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter.getter,在数 ...

随机推荐

  1. 【外包杯】【无语的报错】意想不到的逗号Unexpected comma.(已解决)

    解决了,答案是没保存,看见那些文件是型号了吗,就是这个原因.

  2. 【GIT】学习day04 | 将本地代码推送到码云仓库中进行管理【外包杯】

    仓库代码页 将本能仓库和码云仓库进行关联 代码组成 git remote add origin 加上下面的地址 将本地仓库的代码推送到码云仓库上 git push -u origin master 之 ...

  3. BI软件是什么?应用BI工具能给企业带来什么

    BI软件是指利用数据挖掘.分析和可视化等技术,将企业内部和外部数据转化为有价值的信息和洞察,以帮助企业支持业务决策和优化业务流程的工具和应用程序.常见的BI软件包括Datainside.QlikVie ...

  4. 明解Java第一章练习题答案

    @ 目录 练习1-1 练习1-2 练习1-3 <明解Java>书籍其他章节答案 练习1-1 如果没有表示程序语句末尾的分号,结果会怎么样呢?请编译程序进行确认. 答:编译器报错 练习1-2 ...

  5. linux笔记一(基础命令)

      总结: 1.ls – List ls会列举出当前工作目录的内容(文件或文件夹),就跟你在GUI中打开一个文件夹去看里面的内容一样. 2.mkdir – Make Directory mkdir 用 ...

  6. [ICPC2014WF]Sensor Network

    题目描述 A wireless sensor network consists of autonomous sensors scattered in an environment where they ...

  7. CH395实现主动ping对端功能(代码及说明)

    目录 1.PING原理 1.1简介 1.2协议 1.3通信流程 2.代码解释 3.工程链接 PING原理 1.简介 PING是基于ICMP(Internet Control Message Proto ...

  8. 【2020】装了VirtualBox后VMware Workstation无法使用SSH连接Centos的解决方法

    装了个VirtualBox,然后发现无法使用Xshell远程Vmware中的centos了,一开始感觉是虚拟网卡冲突了,发现把VirtualBox的虚拟网卡禁用就可以使用,但是好麻烦啊??每次我特么要 ...

  9. GPT-4多模态大型语言模型发布

    GPT-4 模型是OpenAI开发的第四代大型语言模型(LLM),它将是一个多模态模型,会提供完全不同的可能性-例如文字转图像.音乐甚至视频.GPT 全称为 Generative Pre-traine ...

  10. 如何从零开始实现TDOA技术的 UWB 精确定位系统(2)

    这是一个系列文章<如何从零开始实现TDOA技术的 UWB 精确定位系统>第2部分. 重要提示(劝退说明): Q:做这个定位系统需要基础么?A:文章不是写给小白看的,需要有电子技术和软件编程 ...