Browse Source

Gitbook Auto Published

willin 6 years ago
parent
commit
6bda6122da
9 changed files with 731 additions and 2 deletions
  1. 6 0
      _sidebar.md
  2. 1 1
      basic/md/gitbook-to-docsify.md
  3. 176 0
      basic/node/cpu.md
  4. 92 0
      basic/pad.md
  5. 267 0
      experience/azure/iot-hub.md
  6. 101 0
      experience/azure/storage.md
  7. 68 0
      experience/azure/web-app.md
  8. 1 1
      index.html
  9. 19 0
      sidebar.js

+ 6 - 0
_sidebar.md

@@ -1,6 +1,7 @@
 - [版权](COPYRIGHT.md)
 - 知识篇
   - [操作系统(OS X)](basic/osx.md)
+  - [优雅地使用iPad开发](basic/pad.md)
   - 必备神器
     - [Brew](basic/resource/brew.md)
     - [OhMyZsh](basic/resource/zsh.md)
@@ -29,6 +30,7 @@
     - [Test](basic/node/test.md)
     - [Benchmark](basic/node/benchmark.md)
     - [造轮子(NPM)篇](basic/node/npm.md)
+    - [CPU调度](basic/node/cpu.md)
   - Markdown
     - [Hexo静态博客搭建](basic/md/hexo.md)
     - [GitBook到Docsify](basic/md/gitbook-to-docsify.md)
@@ -73,6 +75,10 @@
     - [重启服务](experience/operation/restarter.md)
     - [版本回退](experience/operation/rollback.md)
     - [CertBot证书](experience/operation/certbot.md)
+  - Azure(Node.js)
+    - [IoT Hub](experience/azure/iot-hub.md)
+    - [Storage](experience/azure/storage.md)
+    - [WebAPP](experience/azure/web-app.md)
   - 进阶
     - [元编程构造优雅解决方案](experience/advanced/meta.md)
     - [Electron桌面应用](experience/advanced/desktop-app.md)

+ 1 - 1
basic/md/gitbook-to-docsify.md

@@ -67,7 +67,7 @@ window.$docsify = {
 
 注意这里的 `alias`, 是设置别名用的. 我们需要为每个创建的目录重定向`_sidebar.md`, 因为 Docsify 默认是从当前目录下去找这个文件的.
 
-写一个简单脚本遍历一下,在根目录创建 `demo.js`
+写一个简单脚本遍历一下,在根目录创建 `sidebar.js`
 
 ```js
 const fs = require('fs');

+ 176 - 0
basic/node/cpu.md

@@ -0,0 +1,176 @@
+# Node.js CPU调度优化
+
+!> Master / Cluster 模式
+
+## 单一服务器多核心分配
+
+假设处理的任务列表如下:
+
+```js
+const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
+```
+
+以10为例,假设服务器为4CPU,那么每个CPU处理的任务分别为:
+
+- CPU1: [1, 2, 3]
+- CPU2: [4, 5, 6]
+- CPU3: [7, 8]
+- CPU4: [9, 0]
+
+```js
+const numCPUs = require('os').cpus().length; // 假设该值为 4
+
+// 处理的任务列表
+const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
+
+// 调度处理代码写在这儿
+// 每个 CPU 分配 N 个任务
+const n = Math.floor(arr.length / numCPUs);
+// 未分配的余数
+const remainder = arr.length % numCPUs;
+
+for (let i = 1; i <= numCPUs; i += 1) {
+  console.log(arr.splice(0, n + (i > remainder ? 0 : 1)));
+}
+```
+
+## Cluster 模式示例
+
+入口文件 `index.js`
+
+```js
+const cluster = require('cluster');
+(async () => {
+  /* eslint global-require:0 */
+  let run;
+  if (cluster.isMaster) {
+    run = require('./cluster/master');
+  } else {
+    run = require('./cluster/worker');
+  }
+  try {
+    await run();
+  } catch (err) {
+    console.trace(err);
+  }
+})();
+```
+
+Master任务: `./cluster/master.js`
+
+```js
+const cluster = require('cluster');
+const numCPUs = require('os').cpus().length;
+
+// 处理的任务列表
+const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
+
+module.exports = async () => {
+  // 调度处理代码写在这儿
+  // 每个 CPU 分配 N 个任务
+  const n = Math.floor(arr.length / numCPUs);
+  // 未分配的余数
+  const remainder = arr.length % numCPUs;
+
+  for (let i = 1; i <= numCPUs; i += 1) {
+    const tasks = arr.splice(0, n + (i > remainder ? 0 : 1));
+    // 将任务编号传递到 Cluster 内启动
+    cluster.fork({ tasks: JSON.stringify(tasks) });
+  }
+  cluster.on('exit', (worker) => {
+    console.log(`worker #${worker.id} PID:${worker.process.pid} died`);
+  });
+};
+```
+
+Cluster任务: `./cluster/worker.js`
+
+```js
+const cluster = require('cluster');
+// 禁止直接启动
+if (cluster.isMaster) {
+  process.exit(0);
+}
+
+module.exports = async () => {
+  const env = process.env.tasks;
+  let tasks = [];
+  if (/^\[.*\]$/.test(env)) {
+    tasks = JSON.parse(env);
+  }
+  if (tasks.length === 0) {
+    // 非法启动, 释放进程资源
+    process.exit(0);
+  }
+  console.log(`worker #${cluster.worker.id} PID:${process.pid} Start`);
+  console.log(tasks);
+};
+```
+
+## 多服务器多核心分配调度
+
+假设处理的任务列表如下:
+
+```js
+const arr = [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, 27, 28, 29, 30, 31, 32];
+```
+
+有多台负载均衡器,仅确定服务器数量,不确定服务器硬件配置.
+
+
+假设目前有3台服务器,分别为 `4` 核心, `6` 核心, `8` 核心.
+
+按照核心性能进行优先调度,那么每个CPU处理的任务分别为:
+
+- 服务器1 (`4` 核心, 1.8GHz)
+  - CPU1: [ 29 ]
+  - CPU2: [ 30 ]
+  - CPU3: [ 31 ]
+  - CPU4: [ 32 ]
+- 服务器2 (`6` 核心, 2.8GHz)
+  - CPU1: [ 1, 2 ]
+  - CPU2: [ 3, 4 ]
+  - CPU3: [ 5, 6 ]
+  - CPU4: [ 7, 8 ]
+  - CPU5: [ 9, 10 ]
+  - CPU6: [ 11, 12 ]
+- 服务器3 (`8` 核心, 2.0GHz)
+  - CPU1: [ 13, 14 ]
+  - CPU2: [ 15, 16 ]
+  - CPU3: [ 17, 18 ]
+  - CPU4: [ 19, 20 ]
+  - CPU5: [ 21, 22 ]
+  - CPU6: [ 23, 24 ]
+  - CPU7: [ 25, 26 ]
+  - CPU8: [ 27, 28 ]
+
+```js
+const os = require('os');
+const numCPUs = os.cpus().length;
+
+// 处理的任务列表
+const arr = [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, 27, 28, 29, 30, 31, 32];
+
+// 调度处理代码写在这儿
+// 处理器主频
+const speed = os.cpus().reduce((sum, cpu) => sum + cpu.speed, 0) / numCPUs;
+// 主机名
+const hostname = os.hostname();
+// 获取内网ip
+const eth0 = os.networkInterfaces().eth0;
+const ip = typeof eth0 === 'undefined' ? '' : eth0.filter(x => x.family === 'IPv4')[0].address;
+
+// ./cluster/master.js
+module.exports = async () => {
+  // 上报服务器信息到公共区域, 如 redis
+
+  // 等待 `3` 台服务器全部上报完成
+  
+  // 性能最高的一台执行任务调度,得到任务列表
+
+  // 写入公共区域,下派任务到其他服务器
+
+  // 下派本地cluster任务
+
+};
+```

+ 92 - 0
basic/pad.md

@@ -0,0 +1,92 @@
+# 优雅地使用平板进行远程OS X编码开发
+
+!> 没有OS X平板(iPad是ios系统)这个问题一直困扰着我.
+
+?> 正所谓,工欲善其事必先利其器. 
+
+## 准备
+
+- 一台平板(iPad/Surface或者其他)
+- 一台苹果(Mac Mini/Macbook Pro或者其他)
+- 一台路由器(需支持动态域名解析,如花生壳),后续我会完善动态域名解析功能
+
+
+## 配置电源选项
+
+![power](https://user-images.githubusercontent.com/1890238/27117064-6ca0b32a-509a-11e7-98fb-db4fa50eeb04.png)
+
+根据上图开启 `唤醒`, 关闭 `睡眠`
+
+```bash
+sudo pmset -a autopoweroff 0
+sudo pmset -a standby 0 
+```
+
+## 配置路由器
+
+### IP地址分配
+
+通过mac地址进行绑定,分配固定ip.
+
+一般是在:
+
+> 路由设置 -> 上网设置 -> 静态IP
+
+![ip](https://cloud.githubusercontent.com/assets/1890238/26823551/99485ffa-4ae0-11e7-8212-e22896fd8adf.jpg)
+
+### 端口转发或DMZ
+
+如果路由支持DMZ主机功能,则不需要进行端口转发.直接将本机设置DMZ主机即可.
+
+![port-forward](https://cloud.githubusercontent.com/assets/1890238/26823706/2e63f1bc-4ae1-11e7-896e-df145d8b4400.jpg)
+
+端口转发的话,设置 `1234` 端口(参考下文js代码).
+
+## 配置动态域名解析
+
+### 路由器+花生壳
+
+注册花生壳域名
+
+![oray](https://cloud.githubusercontent.com/assets/1890238/26823557/a37f3f5c-4ae0-11e7-8d53-14a591190348.png)
+
+路由器配置花生壳
+
+很简单,填入用户名密码和域名.
+
+注意下面的两个时间我填的都是 10 分钟.
+
+![router-oray](https://cloud.githubusercontent.com/assets/1890238/26823629/de357cc4-4ae0-11e7-9e23-5652f2a6aa48.jpg)
+
+## 设置唤醒应用
+
+```js
+const http = require('http');
+const { execSync } = require('child_process');
+http.createServer((req, res) => {
+  res.writeHead(200, { 'Content-type': 'application/json' });
+  try {
+    execSync('caffeinate -u -t 1');
+    res.end('{status:1}');
+  }
+  catch (e) {
+    res.end('{status:0}');
+  }
+}).listen(1234);
+```
+
+假如你的动态解析域名是 `willin.wang`
+
+如果电脑进入睡眠了,用手机访问下面的地址,即可进行唤醒.
+
+```
+http://willin.wang:1234/
+```
+
+## 祭出神器
+
+TeamViewer, 配置无人值守和轻松访问.
+
+![teamviewer](https://user-images.githubusercontent.com/1890238/27117314-adf4255e-509b-11e7-904b-b751ec392b32.png)
+
+享受吧.

+ 267 - 0
experience/azure/iot-hub.md

@@ -0,0 +1,267 @@
+# Azure IoT Hub开发指南
+
+!> IOT Hub应用实际开发过程中的一些注意细节
+
+
+资源:
+
+- 创建设备: <https://www.npmjs.com/package/azure-iothub>
+- IoT Hub(基于Event Hubs)消息管理: <https://www.npmjs.com/package/azure-event-hubs>
+- 开发调试工具: <https://www.npmjs.com/package/iothub-explorer>
+
+## 简单发送接收示例
+
+### 1. 注册设备
+
+```js
+const iothub = require('azure-iothub');
+
+const registry = iothub.Registry.fromConnectionString('[connectionString]');
+
+const device = new iothub.Device(null);
+device.deviceId = '[deviceId]';
+
+function printDeviceInfo(err, deviceInfo, res) {
+  if (deviceInfo) {
+    console.log(JSON.stringify(deviceInfo, null, 2));
+    console.log(`Device id: ${deviceInfo.deviceId}`);
+    console.log(`Device key: ${deviceInfo.authentication.symmetricKey.primaryKey}`);
+  }
+}
+
+// 删除设备 registry.delete(deviceId, (err, deviceInfo, res) => {});
+registry.create(device, (err, deviceInfo, res) => {
+  if (err) {
+    registry.get(device.deviceId, printDeviceInfo);
+  }
+  if (deviceInfo) {
+    printDeviceInfo(err, deviceInfo, res);
+  }
+});
+```
+
+### 2. 模拟设备发送消息
+
+```js
+const clientFromConnectionString = require('azure-iot-device-mqtt').clientFromConnectionString;
+const Message = require('azure-iot-device').Message;
+
+const connectionString = 'HostName=[修改连接主机];DeviceId=[deviceID];SharedAccessKey=[连接密钥]';
+
+const client = clientFromConnectionString(connectionString);
+
+function printResultFor(op) {
+  return function printResult(err, res) {
+    if (err) console.log(`${op} error: ${err.toString()}`);
+    if (res) console.log(`${op} status: ${res.constructor.name}`);
+  };
+}
+
+const connectCallback = function (err) {
+  if (err) {
+    console.log(`Could not connect: ${err}`);
+  } else {
+    console.log('Client connected');
+
+    // Create a message and send it to the IoT Hub every second
+    setInterval(() => {
+      const windSpeed = 10 + (Math.random() * 4);
+      const data = JSON.stringify({ deviceId: 'myFirstNodeDevice', windSpeed });
+      const message = new Message(data);
+      console.log(`Sending message: ${message.getData()}`);
+      client.sendEvent(message, printResultFor('send'));
+    }, 1000);
+  }
+};
+
+client.open(connectCallback);
+```
+
+### 3. 服务器端接收消息
+
+```js
+const EventHubClient = require('azure-event-hubs').Client;
+
+const connectionString = 'HostName=[修改连接主机];SharedAccessKeyName=iothubowner;SharedAccessKey=[修改连接密钥]';
+
+const printError = function (err) {
+  console.log(err.message);
+};
+
+const printMessage = function (message) {
+  console.log('Message received: ');
+  console.log(JSON.stringify(message.body));
+  Object.getOwnPropertyNames(message).forEach((x) => {
+    console.log(x, message[x]);
+  });
+  console.log('');
+};
+
+const client = EventHubClient.fromConnectionString(connectionString);
+
+client.open()
+    .then(client.getPartitionIds.bind(client))
+    .then(partitionIds => partitionIds.map(partitionId => client.createReceiver('$Default', partitionId, { startAfterTime: Date.now()}).then((receiver) => {
+      console.log(`Created partition receiver: ${partitionId}`);
+      receiver.on('errorReceived', printError);
+      receiver.on('message', printMessage);
+    })))
+    .catch(printError);
+```
+
+注意:
+
+- 客户端传的`properties`,在消息体中是`message.applicationProperties`
+- `message`包含的属性如下:
+
+```js
+[ 'partitionKey',
+  'body',
+  'enqueuedTimeUtc',
+  'offset',
+  'properties',
+  'applicationProperties',
+  'sequenceNumber',
+  'annotations',
+  'systemProperties' ]
+```
+
+消息体示例:
+
+```bash
+Message received:
+partitionKey undefined
+body { deviceId: 'myFirstNodeDevice', windSpeed: 10.51685587945142 }
+enqueuedTimeUtc 2017-06-13T01:21:02.519Z
+offset 73240
+properties undefined
+applicationProperties { asdf: 'asdfz' }
+sequenceNumber 182
+annotations { 'x-opt-sequence-number': 182,
+  'x-opt-offset': '73240',
+  'x-opt-enqueued-time': 2017-06-13T01:21:02.519Z,
+  'iothub-connection-device-id': 'myFirstNodeDevice',
+  'iothub-connection-auth-method': '{ "scope": "device", "type": "sas", "issuer": "iothub" }',
+  'iothub-connection-auth-generation-id': 'xxxxxxx',
+  'iothub-enqueuedtime': 2017-06-13T01:21:02.786Z,
+  'iothub-message-source': 'Telemetry' }
+systemProperties undefined
+```
+
+## 配置路由(需要Event Hubs)
+
+### 1. 创建Event Hubs
+
+### 2. 从事件中心创建实体
+
+![eventhubs-entities](https://user-images.githubusercontent.com/1890238/27019465-566b06d4-4efe-11e7-8a74-240c0c523ac4.png)
+
+### 3. 获取连接字符串
+
+点击进入已创建的实体
+
+![eventhubs-key](https://user-images.githubusercontent.com/1890238/27019487-89f17e8e-4efe-11e7-815c-c3d62a3213ef.png)
+
+不要从别处获得连接字符串,因为可能无法连接. 最终获得的连接字符串应当包含`EntityPath`字段,类似:
+
+```
+Endpoint=sb://xxxx.servicebus.chinacloudapi.cn/;SharedAccessKeyName=iothubroutes_xxxx;SharedAccessKey=xxxx;EntityPath=xxxx
+```
+
+### 4. 创建Endpoint
+
+![iothub-endpoints](https://user-images.githubusercontent.com/1890238/27019555-23edcb5a-4eff-11e7-89e6-57f88d241612.png)
+
+将 Event Hubs 里的事件关联到 IoT Hub
+
+### 5. 创建路由
+
+![iothub-route](https://user-images.githubusercontent.com/1890238/27019570-5238cd52-4eff-11e7-932f-78a8a97d0246.png)
+
+### 示例代码
+
+#### 1. 修改刚才的发送示例
+
+```js
+const clientFromConnectionString = require('azure-iot-device-mqtt').clientFromConnectionString;
+const Message = require('azure-iot-device').Message;
+
+const connectionString = 'HostName=[修改连接主机];DeviceId=[deviceID];SharedAccessKey=[连接密钥]';
+
+const client = clientFromConnectionString(connectionString);
+
+function printResultFor(op) {
+  return function printResult(err, res) {
+    if (err) console.log(`${op} error: ${err.toString()}`);
+    if (res) console.log(`${op} status: ${res.constructor.name}`);
+  };
+}
+
+const connectCallback = function (err) {
+  if (err) {
+    console.log(`Could not connect: ${err}`);
+  } else {
+    console.log('Client connected');
+
+    // Create a message and send it to the IoT Hub every second
+    setInterval(() => {
+      const windSpeed = 10 + (Math.random() * 4);
+      const data = JSON.stringify({ deviceId: 'myFirstNodeDevice', windSpeed });
+      const message = new Message(data);
+      // 随机发送到路由或默认事件上
+      if (Math.round(Math.random()) === 1) {
+        message.properties.add('route', 'test');
+      }
+      console.log(`Sending message: ${message.getData()}`);
+      client.sendEvent(message, printResultFor('send'));
+    }, 1000);
+  }
+};
+
+client.open(connectCallback);
+```
+
+#### 2. IoT Hub 侦听启动
+
+无需修改,直接启动
+
+#### 3. Event Hubs 侦听启动
+
+复制 IoT Hub 侦听源码,修改连接字符串:
+
+
+```js
+const EventHubClient = require('azure-event-hubs').Client;
+
+// const connectionString = 'HostName=[修改连接主机];SharedAccessKeyName=iothubowner;SharedAccessKey=[修改连接密钥]';
+const connectionString = 'Endpoint=[sb://修改连接主机.servicebus.chinacloudapi.cn/];SharedAccessKeyName=[修改连接策略];SharedAccessKey=[x修改连接密钥];EntityPath=[事件实体]'
+
+const printError = function (err) {
+  console.log(err.message);
+};
+
+const printMessage = function (message) {
+  console.log('Message received: ');
+  console.log(JSON.stringify(message.body));
+  console.log(message);
+  console.log('');
+};
+
+const client = EventHubClient.fromConnectionString(connectionString);
+
+client.open()
+    .then(client.getPartitionIds.bind(client))
+    .then(partitionIds => partitionIds.map(partitionId => client.createReceiver('$Default', partitionId, { startAfterTime: Date.now()}).then((receiver) => {
+      console.log(`Created partition receiver: ${partitionId}`);
+      receiver.on('errorReceived', printError);
+      receiver.on('message', printMessage);
+    })))
+    .catch(printError);
+```
+
+#### 测试结果
+
+- 发送到默认路由的,只能被IoT Hub侦听应用捕获.
+- 发送到刚才配置的测试路由的,只能被Event Hubs侦听应用捕获.
+
+至此,完成路由转发.

+ 101 - 0
experience/azure/storage.md

@@ -0,0 +1,101 @@
+# Azure Blob文件上传
+
+
+azure-storage官方文档: <http://azure.github.io/azure-storage-node/>
+
+## 建立连接
+
+有3种方式(文档中未提及):
+
+### 1. 通过环境变量
+
+```bash
+AZURE_STORAGE_CONNECTION_STRING="valid storage connection string" node app.js
+```
+
+应用程序内:
+
+```js
+const azure = require('azure-storage');
+const blobService = azure.createBlobService();
+// code here
+```
+
+### 2.连接字符串
+
+```js
+const azure = require('azure-storage');
+const blobService = azure.createBlobService('connectionString'); // 类似: DefaultEndpointsProtocol=https;AccountName=*****;AccountKey=*****;EndpointSuffix=*****.core.chinacloudapi.cn
+// code here
+```
+
+### 3.账号+密钥
+
+```js
+const azure = require('azure-storage');
+const blobService = azure.createBlobService('storageAccount', 'storageAccessKey', 'storageHost'); 
+// code here
+```
+
+## 上传示例
+
+因为POST请求接收到的大部分是Stream.所以采用Sream的方式上传.
+
+```js
+// azure.js
+const azure = require('azure-storage');
+const { getDefer } = require('@dwing/common');
+
+const blobService = azure.createBlobService('accountName', 'accessKey', 'host');
+
+exports.createBlockBlobFromStream = (container, filename, blob) => {
+  const deferred = getDefer();
+  blob.on('error', (err) => {
+    deferred.reject(err);
+  });
+  blob.pipe(blobService.createWriteStreamToBlockBlob(container, filename));
+  blob.on('end', () => {
+    deferred.resolve(1);
+  });
+  return deferred.promise;
+};
+```
+
+测试代码:
+
+```js
+// demo.js
+const { createBlockBlobFromStream } = require('./azure');
+const fs = require('fs');
+const path = require('path');
+
+const stream = fs.createReadStream(path.join(__dirname, '/testfile'));
+
+(async () => {
+  const result = await createBlockBlobFromStream('container', 'filename', stream);
+  console.log(result);
+})();
+```
+
+在 AirX 项目中的实际使用:
+
+<https://github.com/AirDwing/node-airx-sdk>
+
+```js
+const SDK = require('@airx/sdk');
+const fs = require('fs');
+
+const sdk = new SDK({
+  SecretId: 'xxxx',
+  SecretKey: 'xxxx'
+});
+
+(async () => {
+  const result = await sdk.upload({
+    auth: 'xxxx',
+    type: 'orgverify',
+    file: fs.createReadStream('PATH/TO/xxx.jpg') // 注意这里, 本地文件可以用 path.join 拼装地址,或者直接用Stream
+  });
+  console.log(result);
+})();
+```

+ 68 - 0
experience/azure/web-app.md

@@ -0,0 +1,68 @@
+# Azure Node.js WebApp 
+
+!> 对官方文档一些需要额外注意的细节整理
+
+
+[在 Azure App Service 中创建 Node.js Web 应用](https://www.azure.cn/documentation/articles/web-sites-nodejs-develop-deploy-mac/)
+
+## 注意事项
+
+### 1. 启动项 npm start
+
+必须以`node`命令执行,且必须以`node`命令打头,如:
+
+```js
+"scripts": {
+  "start": "node PATH/app.js"
+}
+```
+
+不能以`cli`工具执行,像这样的是无法执行的:
+
+```js
+"scripts": {
+  "start": "gitbook serve"
+}
+```
+
+还有这样:
+
+```js
+"scripts": {
+  "start": "NODE_ENV=production node PATH/app.js"
+}
+```
+
+### 2. 不能指定端口号
+
+```js
+app.listen(3000); // 抱歉,发布后无法访问
+```
+
+必须引用`process.env.PORT`,像这样写:
+
+```js
+app.listen(process.env.PORT || 3000);
+// 部署后传入绑定的PORT类似: \\.\pipe\69b6d648-e61e-4da2-9de5-fb797348d3fa 
+```
+
+
+## 环境变量配置
+
+`NODE_ENV` 或者其他环境变量的配置位于:
+
+> WebApp -> 应用程序设置 -> 应用设置
+
+![](https://cloud.githubusercontent.com/assets/1890238/26770714/ac0ede34-49eb-11e7-8850-9c9740dc48d2.png)
+
+## 指定node/npm版本
+
+`package.json`中添加:
+
+```js
+"engines": {
+  "node": ">= 8.0.0",
+  "npm": ">= 5.0.0"
+}
+```
+ 

+ 1 - 1
index.html

@@ -31,6 +31,7 @@
       '/basic/resource/_sidebar.md': '/_sidebar.md',
       '/experience/_sidebar.md': '/_sidebar.md',
       '/experience/advanced/_sidebar.md': '/_sidebar.md',
+      '/experience/azure/_sidebar.md': '/_sidebar.md',
       '/experience/design/_sidebar.md': '/_sidebar.md',
       '/experience/operation/_sidebar.md': '/_sidebar.md',
       '/experience/project/_sidebar.md': '/_sidebar.md',
@@ -71,7 +72,6 @@
           }
           ads.push(paragraphs[paragraphs.length-1]);
           for(var i = 0; i < ads.length; i += 1) {
-            console.log(ads[i]);
             ads[i].insertAdjacentHTML('afterend', '<ins class="adsbygoogle" style="display:block;margin: 1.5em auto;" data-ad-client="ca-pub-5059418763237956" data-ad-slot="9518721243" data-ad-format="auto"></ins>');
             (adsbygoogle = window.adsbygoogle || []).push({});   
           }

+ 19 - 0
sidebar.js

@@ -0,0 +1,19 @@
+const fs = require('fs');
+const path = require('path');
+
+const root = path.join(__dirname);
+const result = {};
+
+function readDirSync(p) {
+  const dir = fs.readdirSync(p);
+  dir.forEach((file) => {
+    const info = fs.statSync(`${p}/${file}`);
+    if (info.isDirectory() && file.indexOf('.') !== 0 && file.indexOf('_') !== 0) {
+      result[`${p.replace(root, '')}/${file}/_sidebar.md`] = '/_sidebar.md';
+      readDirSync(`${p}/${file}`);
+    }
+  });
+}
+
+readDirSync(root);
+console.log(result);