# 2020年08月技术日常

# 2020/08/30 周日

# npm run 运行多条命令 && 不生效的问题

在项目中除了默认的 webpack-dev-server 外,我们还想同时运行mock接口的node服务

// npm run 同时执行下面的两条命令
// webpack-dev-server --config webpack.dev.js
// nodemon ./mock/index.js

一般 & 表示并列执行,&& 表示两条命令顺序执行。使用 && 的形式设置对应的script,如下

"scripts": {
  "server": "nodemon ./mock/index.js && webpack-dev-server --config webpack.dev.js"
}

但实际运行 npm run server 时,只运行了第一条命令,我的是mac,不知道是不是node版本的问题。网上找了下, && 存在兼容性问题。建议使用 concurrently 来代替,concurrently 跨平台兼容

// 先安装 concurrently
// npm install concurrently --save
// 修改 package.json 如下
"server": "concurrently \"nodemon ./mock/index.js\" \"webpack-dev-server --config webpack.dev.js\""

这样就可以了,运行效果如下

npm_run_multi.png

注意:&& 是按顺序执行多条命令, concurrently 是并列执行多条命令,对于跨平台的顺序执行,可以使用 npm-run-all

参考:npm并行&串行执行多个scripts命令 (opens new window)

# mac设置vscode默认打开的浏览器

vscode中,安装 Live Server 插件后,在html文件里,右键 open with Live Server,可以将网页使用http服务在浏览器里面打开。

我电脑里面默认打开的是safari,但我一般习惯用Chrome,这个默认浏览器的设置需要在mac系统里设置

点击屏幕左上角苹果标志 => 系统偏好设置 => 通用 => 默认网页浏览器 选择 Google Chrome 即可

default_browser.png

# 2020/08/24 周一

# 怎么在老项目中加入eslint规则,并可以保存后自动fix

在 "vue-cli构建项目时选择不同的eslint规则会有什么区别?" 中,我们有了解到,不同的eslint配置项,会对应不同的npm包,eslintConfig 中对应的 extends 也不一样。我们只要找出某个配置特有的npm包,以及配置,就可以在旧项目中,引入对应的eslint配置,结合vscode eslint插件就可以做到保存后自动fix

  1. 新建一个目录eslint-fix-test,cd到该目录,使用 npm init -y 创建一个新的package.json
  2. 我们把 ESLint + Prettier 的package.json配置拷贝到这个package.json中
// ESLint + Prettier package.json配置
 "devDependencies": {
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/eslint-config-prettier": "^6.0.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-prettier": "^3.1.3",
    "eslint-plugin-vue": "^6.2.2",
    "prettier": "^1.19.1"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended",
      "@vue/prettier"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  }
  1. npm install 安装依赖
  2. 在该目录下写一个test.vue文件,或test.js文件,特意写的很随意,看eslint是否报错

一般情况下,npm install 后,如果vscode eslint插件配置正确,会弹出如下弹窗,提示是否允许eslint修复

eslint-fix-1.png

注意:

  1. 如果没有安装配置好vscode的eslint插件,请先安装,可以参考我之前的笔记: 2020 vscode配置eslint保存后自动fix (opens new window)
  2. 如果没有生效,可以尝试新开一个vscode窗口,直接打开eslint-fix-test目录,有时候目录层级深了,会不起作用
  3. 如果还是没生效,可以尝试彻底关闭 vscode,再重新打开
  4. 有时候刚打开项目,eslint插件可能需要时间加载,等个几秒钟才会工作

下面是我测试时,eslint插件工作正常的示例

eslint-fix-2.png

保存后会自动fix,由于两个变量没使用还是会报警告,我用console.log打印了下,就没warning了

eslint-fix-3.png

综上,只要你知道某个eslint配置所需的npm包及eslintConfig.extends配置,那么就可以在项目中任意使用eslint了,完整测试demo可以从github 下载 eslint-fix-test | github (opens new window)

# vue-cli构建项目时选择不同的eslint规则会有什么区别?

当我们使用 vue create xxx 时,会让选择是否需要某个模块,如下图

vue_create_perset.png

如果有使用空格选择 Linter / Formatter,那么后面的流程会让你选择一种eslint规则

? Pick a linter / formatter config: (Use arrow keys)
❯ ESLint with error prevention only 
  ESLint + Airbnb config 
  ESLint + Standard config 
  ESLint + Prettier 

加上不使用Linter / Formatter,总共有5中情况,我们都逐一试试,看生成的package.json有什么不同

注意除了eslint规则选择不一样外,其他都一样。vue-cli 版本 4.5.0,vue 版本 2.x

// 统一选择 lint on save,配置放到package.json,不单独生成elsint配置文件
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In package.json

# 不使用eslint时

  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-vuex": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "less": "^3.0.4",
    "less-loader": "^5.0.0",
    "vue-template-compiler": "^2.6.11"
  },

# ESLint with error prevention only

// 注释掉除eslint功能之外的配置
"scripts": {
  // "serve": "vue-cli-service serve",
  // "build": "vue-cli-service build",
  "lint": "vue-cli-service lint"
},
"devDependencies": {
    // "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    // "@vue/cli-plugin-router": "~4.5.0",
    // "@vue/cli-plugin-vuex": "~4.5.0",
    // "@vue/cli-service": "~4.5.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    // "less": "^3.0.4",
    // "less-loader": "^5.0.0",
    // "vue-template-compiler": "^2.6.11"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },

# ESLint + Airbnb config

 "devDependencies": {
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/eslint-config-airbnb": "^5.0.2",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-import": "^2.20.2",
    "eslint-plugin-vue": "^6.2.2",
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "@vue/airbnb"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },

多了一个.editorconfig

[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
# ESLint + Standard config
 "devDependencies": {
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/eslint-config-standard": "^5.1.2",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-import": "^2.20.2",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-promise": "^4.2.1",
    "eslint-plugin-standard": "^4.0.0",
    "eslint-plugin-vue": "^6.2.2",
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "@vue/standard"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },

多了一个.editorconfig

[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

# ESLint + Prettier

  "devDependencies": {
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/eslint-config-prettier": "^6.0.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-prettier": "^3.1.3",
    "eslint-plugin-vue": "^6.2.2",
    "prettier": "^1.19.1",
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended",
      "@vue/prettier"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },

我们使用上面相同的配置,只是不选择将eslint配置放到paackage.json,而是选择 In dedicated config files 使用专门的配置文件,他会额外生成一个 .eslintrc.js 存放对应的eslint配置

// .eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true
  },
  extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"],
  parserOptions: {
    parser: "babel-eslint"
  },
  rules: {
    "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
    "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off"
  }
};

# ESLint配置配置对比总结

// 1. ESLint with error prevention only 
// - devDependencies
"@vue/cli-plugin-eslint": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
// - eslintConfig
"extends": [
  "plugin:vue/essential",
  "eslint:recommended"
],

// 2. ESLint + Airbnb config 
// - devDependencies
"@vue/cli-plugin-eslint": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"@vue/eslint-config-airbnb": "^5.0.2",
"eslint-plugin-import": "^2.20.2",
// - eslintConfig
"extends": [
  "plugin:vue/essential",
  "@vue/airbnb"
],

// 3. ESLint + Standard config 
// - devDependencies
"@vue/cli-plugin-eslint": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"@vue/eslint-config-standard": "^5.1.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",

// - eslintConfig
"extends": [
  "plugin:vue/essential",
  "@vue/standard"
],

// 4. ESLint + Prettier
// - devDependencies
"@vue/cli-plugin-eslint": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"@vue/eslint-config-prettier": "^6.0.0",
"eslint-plugin-prettier": "^3.1.3",
"prettier": "^1.19.1",
// - eslintConfig
"extends": [
  "plugin:vue/essential",
  "eslint:recommended",
  "@vue/prettier"
],

# 怎么用vue写一个组件库,类似element

下面通过一个简单的示例来看怎么写一个vue组件库

# 1.组件库目录结构

以element为例,下面是一个简单的目录结构

├── examples # 用于测试组件,demo展示(可以直接是一个完整的vue项目)
├── packages # 用于存放编写的组件
│   └── my-button # 封装的组件,具体写法参见element源码
│       ├── src    
│       │   └── main.vue  # 单文件组件,组件
│       └── index.js  # install方法,用于Vue.use()引入单个组件
└── src
    └── index.js # 入口文件

# 2.导出组件库

我们写好组件后,主要问题是要怎么导出(export)供其他项目使用,一般在 src/index.js 里进行处理

// src/index.js 示例,主要是引入(import)组件再导出(export),包括Vue.use所需的 install函数处理
import MyButton from "../packages/my-button/index.js";
const components = [MyButton];

// Vue.use() 一次性安装所有组件
const install = function(Vue) {
  if (install.installed) return;
  components.forEach(component => Vue.use(component));
  // 如果没有在src/index.js里实现install方法,就需要使用下面的
  // components.forEach(component => Vue.component(component.name, component))
};

// 直接给浏览器或 AMD loader 使用
if (typeof window !== undefined && window.Vue) {
  install(window.Vue);
}

export default {
  install, // 用于ES modules,import Vue 后直接使用 Vue.use()
  MyButton // 支持解构赋值按需引入单个组件
};
# 3.本地测试组件

假设我们这个组件命名为vue-chart,那可以在 examples/src/mian.js 直接使用

// examples/src/mian.js
// 引入所有组件
import VueChart from '../../src/index.js'
Vue.use(VueChart)

// 按需引入单个组件
import { MyButton } from '../../src/index.js'
Vue.use(MyButton)

引入后,我们在 App.vue里直接使用该组件

<!-- App.vue -->
<template>
  <div id="app">
    <my-button id="btn">测试按钮</my-button>
  </div>
</template>

这里可能涉及到 examples 目录 npm run serve 时,提示 eslint 错误

eslint-error.png

因为 examples 是一个独立的vue项目,引入了外部 packages 目录的文件,如果 packages 下组件不符合eslint规则就需要在项目根目录的package.json里配置eslint规则了,与examples项目的eslint规则保持一致。参考我之前的笔记:"怎么在老项目中加入eslint规则,并可以保存后自动fix"

这样使用eslint处理 packages 目录下的代码吗,再运行 examples 目录下的 npm run serve 就可以正常加载组件了

# 4.打包构建umd

上面我们本地测试了,但他不能直接在普通网页中通过引入某个js文件来使用,这就需要使用Vue CLI来打包构建了

使用 vue-cli 打包成 lib,参考文档 构建目标 | Vue CLI (opens new window)

由于我们的项目结构不是通过vue-cli生成的,所以就算使用npm install @vue/cli --save,安装 @vue/cli,也无法使用vue-cli-service服务,因为@vue/cli这个包是一个用于生成脚手架项目的。你没有用它生成项目,如果不安装对应的npm包是无法使用vue-cli-servie的,我们直接从vue create 生成的项目中把 devDependencies、dependencies,配置移动到根目录的package.json里,再npm install

// dependencies
"dependencies": {
  "core-js": "^3.6.5",
  "vue": "^2.6.11"
},
// devDependencies
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"less": "^3.0.4",
"less-loader": "^5.0.0",
"vue-template-compiler": "^2.6.11"
// browserslist
"browserslist": [
  "> 1%",
  "last 2 versions",
  "not dead"
],

新增一个lib打包命令

"scripts": {
  "build:lib": "vue-cli-service build --mode lib --target lib --dest lib src/index.js"
}   

理论上 npm run build:lib 会在当前目录下创建lib目录,且包含4个文件

  • lib/myLib.common.js:一个给打包器用的 CommonJS 包 (不幸的是,webpack 目前还并没有支持 ES modules 输出格式的包)
  • lib/myLib.umd.js:一个直接给浏览器或 AMD loader 使用的 UMD 包
  • lib/myLib.umd.min.js:压缩后的 UMD 构建版本
  • lib/myLib.css:提取出来的 CSS 文件 (可以通过在 vue.config.js 中设置 css: { extract: false } 强制内联)

实际我这里打包后没有生成css文件

vue_chart_build_lib.png

一般将打包后的文件 xx.umd.js 引入到项目中就可以使用了,来看看示例

<!-- docs/index.html -->
<body>
    <div id="app">
        <my-button>我的按钮</my-button>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script src="../lib/vue-chart.umd.js"></script>
    <script>
        const app = new Vue({
            el: '#app'
        })
    </script>
</body>

vue_chat_test1.png

vue项目中引用

// examples/src/main.js
import VueChart from "../../lib/vue-chart.umd.js";
Vue.use(VueChart);

效果如下

vue_chat_test2.png

# 5.上传npm包

我们可以把包弄成npm包,在其他vue项目中,可以直接引入,将 package.json 的 main.js 设置为umd模块地址,并加上unpkg参数,用于cdn直接引入

// package.json
"name": "@guoqzuo/vue-chart", // 将项目名称改为加上自己作用域的包
"version": "0.0.2", // 设置项目版本,注意每次修改版本,package-lock.json的版本也要改
// 指定npm包入口,当我们import某个npm包时,导入的文件就是这个main指定的文件
"main": "lib/vue-chart.umd.min.js"
// cdn相关
"unpkg": "lib/vue-chart.umd.min.js",

关于unpkg

unpkg_info.png

修改好后,我们来发布这个npm包,注意:需要先有对应的npm账号,我的npm账号是 guoqzuo,这里在项目根目录新建一个publish.sh脚本来发布

#!/usr/bin/env bash
npm config get registry # 检查仓库镜像库
npm config set registry=http://registry.npmjs.org
echo '请进行登录相关操作:'
npm login # 登陆
# npm login --scope=@guoqzuo # 设置登录作用域
echo "-------publishing-------"
npm publish --access public # 发布
npm config set registry=https://registry.npm.taobao.org # 设置为淘宝镜像
echo "发布完成"
exit

一般新建的publish.sh是没有执行权限的,使用 chmod +x publish.sh 添加可执行权限,再 ./publish.sh 执行发布,发布过程中,需要登录npm账号、密码等即可

vue_chart_npm_push.png

我们这里examples目录也跟着发布上传了,可以加参数来限定上传到npm包的文件。细节方面后面在优化。我们可以看到npm官网有我们刚上传的 @guoqzuo/vue-chart (opens new window)

guoqzuo_vue_chart.png

测试npm包,在vue项目中先安装

# 这里建议加@latest,因为有时候发布后有延时
# 直接npm i @guoqzuo/vue-chart 后可能还是原来的旧版本
npm i @guoqzuo/vue-chart@latest

安装完成后,再在 main.js 里引入,npm run serve 后可正常渲染

// examples/src/main.js
import VueChart from "@guoqzuo/vue-chart";
Vue.use(VueChart);

另外,我们再测试下 unpkg cdn是否生效

<!-- docs/index.html -->
<body>
    <div id="app">
        <my-button>我的按钮</my-button>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <!-- <script src="../lib/vue-chart.umd.js"></script> -->
     <script src="https://unpkg.com/@guoqzuo/vue-chart@latest/lib/vue-chart.umd.min.js"></script>
    <script>
        const app = new Vue({
            el: '#app'
        })
    </script>
</body>

测试可正常运行,以上完成了一个基本vue组件库的开发流程。后面再慢慢优化,完整代码已上传github,参见 zuoxiaobai/vue-chart: vue echarts library (opens new window)

参考:

# 2020/08/21 周五

# 怎么写出类似element官网那样可以实时看运行效果的文档

最近想做一个文档,把项目中之前实现过的图表组件整合成一个文档。有两个目的:

  1. 让产品/UI在做新功能时,直接从里面挑。不要弄一些不好实现的,需要很大工作量的图表。(虽然说了让产品看echarts官网,但基本不看)
  2. 团队技术沉淀,做了这么多图表,各种属性的配置没有说明文档,每次都是自己调参数。如果每个图表都有说明、笔记,那后面维护的人就可以快速上手了。

对于UI组件、echarts图表相关文档来说,能够实时看具体显示效果是必须的。它是将文档和demo合并在一起,这样更有说服力。那怎么写出类似的文档呢?

# docsify

markdown语法,可嵌入vue代码。基础用法可以参考文档: docsify (opens new window),这里介绍下嵌入vue代码需要注意的问题

由于docsify入口是index.html,本身不支持ES modules,假设vue示例代码中,需要引入echarts模块,是无法import直接运行的。需要通过在index.html引入js来引入组件。有一个比较好的示例,可以参考 ve-charts (opens new window), 可以把源码clone下来跑一跑。通过 npm run build:lib 打包生成 umd 形式的js文件,直接引入即可。

<!-- index.html部分代码 -->
<script src="//cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
<!-- vuep docsify插件,用于实时编辑、显示代码 -->
<script src="//cdn.jsdelivr.net/npm/vuep/dist/vuep.min.js"></script>
<!-- vuep默认是CommonJS规范,需要使用 module.exports,加babel后可以使用ES Modules的export default -->
<script src="https://unpkg.com/babel-standalone/babel.min.js"></script>
<script src="//unpkg.com/echarts@latest/dist/echarts.min.js"></script>
<!-- ve-echarts组件库输出的umd形式js文件,只载入这个js即可完整引入ve-echarts组件库 -->
<script src="./lib/ve-charts.umd.min.js"></script>

README.md

<!-- README.md 里面嵌入vue代码 -->
<vuep template="#basicLine" :options="{ theme: 'vue', lineNumbers: false }"></vuep>

<script v-pre type="text/x-template" id="basicLine">
<template>
  <ve-line-chart :data="chartData" />
</template>

<script>
  export default {
    created () {
      this.chartData = {
        dimensions: {
          name: 'Week',
          data: ['Mon.', 'Tue.', 'Wed.', 'Thu.', 'Fir.', 'Sat.', 'Sun.']
        },
        measures: [{
          name: 'Vue',
          data: [30, 40, 35, 50, 49, 70, 90]
        }]
      }
    }
  }
</script>

效果如下图

dosify_vuep_ve_charts.png

# vuepress

vuepress (opens new window) 是一个静态站点生成器,用于vue官网文档生成。可以内嵌vue代码,Using Vue in Markdown (opens new window)

# element官网源码

ElementUI官网 examples | github (opens new window)

alert文档源码 (opens new window) 为例

## Alert 警告

用于页面中展示重要的提示信息。

### 基本用法

页面中的非浮层元素,不会自动消失。

:::demo Alert 组件提供四种主题,由`type`属性指定,默认值为`info`<template>
  <el-alert
    title="成功提示的文案"
    type="success">
  </el-alert>
  <el-alert
    title="消息提示的文案"
    type="info">
  </el-alert>
  <el-alert
    title="警告提示的文案"
    type="warning">
  </el-alert>
  <el-alert
    title="错误提示的文案"
    type="error">
  </el-alert>
</template>
:::

### 主题

渲染效果如下

elementui_doc_source.png

它是自己写的一套构建程序,将tpl转vue,md解析时,内嵌vue demo会自动渲染代码

// 将 官网.tpl单文件组件转.vue
// /examples/pages/template/component.tpl 就是element组件文档的入口
// https://github.com/ElemeFE/element/blob/dev/build/bin/i18n.js
'use strict';

var fs = require('fs');
var path = require('path');
var langConfig = require('../../examples/i18n/page.json');

langConfig.forEach(lang => {
  try {
    fs.statSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`));
  } catch (e) {
    fs.mkdirSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`));
  }

  Object.keys(lang.pages).forEach(page => {
    var templatePath = path.resolve(__dirname, `../../examples/pages/template/${ page }.tpl`);
    var outputPath = path.resolve(__dirname, `../../examples/pages/${ lang.lang }/${ page }.vue`);
    var content = fs.readFileSync(templatePath, 'utf8');
    var pairs = lang.pages[page];

    Object.keys(pairs).forEach(key => {
      content = content.replace(new RegExp(`<%=\\s*${ key }\\s*>`, 'g'), pairs[key]);
    });

    fs.writeFileSync(outputPath, content);
  });
});

/examples/pages/template/component.tpl 部分代码

<el-scrollbar class="page-component__nav">
  <side-nav :data="navsData[lang]" :base="`/${ lang }/component`"></side-nav>
</el-scrollbar>
<div class="page-component__content">
  <!-- 像 alert、button 文档切换,就是切的这里的路由 -->
  <router-view class="content"></router-view>
  <footer-nav></footer-nav>
</div>

对应的路由代码

// element/examples/route.config.js为alert等文档添加路由部分代码
function addRoute(page, lang, index) {
  const component = page.path === '/changelog'
    ? load(lang, 'changelog')
    : loadDocs(lang, page.path); // 根据path载入对应的docs/zh-CN/xx.md 
  let child = {
    path: page.path.slice(1),
    meta: {
      title: page.title || page.name,
      description: page.description,
      lang
    },
    name: 'component-' + lang + (page.title || page.name),
    component: component.default || component 
  };

  route[index].children.push(child);
}

这里涉及webpack按需加载,构建打包相关,比如怎么将md转换vue,待后续研究

# 2020/08/14 周五

# el-tabs跳转之前先弹窗筛选后再跳转

增加before-leave钩子,它除了支持true,false外,还支持promise resolve和reject,可以将调用js弹窗组件函数改为promise,如果非提交表单关闭弹窗就reject,否则resove表单数据。调用该函数时使用try catch可以很好的处理筛选组件弹窗确定后再跳转到对应tab的逻辑。

# FileReader实例的readAsDataURL与createObjectURL区别

都是读取blob数据, FileReader实例的readAsDataURL下载文件会有限制,超过一定大小会出错,而window.URL.createObjectURL却没有限制

FileReader.readAsDataURL 读取文件之后是 base64 编码的字符串,这个是不能直接作为 src 使用的,要直接使用还应当拼接响应的 MIME Type 前缀,比如 data:audio/ogg; 这是 .ogg 格式的前缀,具体是什么前缀取决于你上传文件的扩展类型了。

其实这里没必要非使用 FileReader 来完成这个需求,读一些小的文件还可以,读大的文件其实不是很好。不如直接使用 URL.createObjectURL() 来创建一个 DOMString,然后直接使用这个 DOMString 即可,不过不要忘记使用完之后通过 URL.revokeObjectURL()方法来释放。

参考:FileReader readAsDataURL读取视频文件 一直预览失败 怎么解决? (opens new window)

# 2020/08/05 周三

# 前端代码规范风格指南

功能组件、目录结构

// 新建文件夹,命名以小写字母开头,驼峰命名
- moduleA // 模块A目录
  - comps // 组件目录
    - CustomerRefuse.vue // 单文件组件命名规则,参考vue风格指南
    // 如果组件内容较多,创建一个文件夹
    // 命名以npm包命名规则一致,全小写、-分隔,建议不超过3个单词
    - no-permission
      - src // 其他资源目录,参考Element组件源码
      - index.vue // 入口或者使用index.js 方便 Vue.use 引入
  - index.vue // 模块A入口

Vue、HTML、JS、CSS编码风格

# 2020/08/04 周二

# 用node实现发送包含echarts图表的邮件

在之前我用node研究过怎么收发邮件,但没有研究过邮件里面是否能包含图表,这次来研究下

首先,我们来看看怎么发邮件

# 准备工作:配置发件人邮箱

我们发送邮件,首先需要有一个邮箱作为发件人,以QQ邮箱为例,我们发邮件使用 916707888@qq.com 作为发件人,我们需要使用程序自动发邮件,所以需要配置发邮件的服务,开启发邮件的POP3/SMTP服务,得到授权码在操作,下面来看看具体过程

登录到qq邮箱,进入管理页面,在设置 - POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务位置,开启POP3/SMTP服务

开启服务后,会得到POP3和IMAP授权码,一个用来收邮件一个用来发邮件,保存好

开启服务.png

gmail邮箱配置

gmail.png

# 使用nodemailer来发送邮件

使用 nodemailer (opens new window) 来发送邮件,首先开一个koa demo

const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()

/**
 * @description 使用nodemailer发送邮件
 * @example
 * POST http://127.0.0.1:9000/sendEmail
 */
router.get('/sendEmail', async ctx => {
  try {
    let sendEmail = require('./sendEmail/index.js')
    await sendEmail()
    ctx.body = {
      msg: '成功'
    }
  } catch (e) {
    ctx.body = {
      msg: e.message
    }
  }
})

app.use(router.routes())
app.listen('9000', () => {
  console.log('server start on 9000 port')
})

发邮件核心代码 sendEmail/index.js

const nodemailer = require('nodemailer');

function sendEmail() {
  return new Promise((resolve, rejected) => {
    // create reusable transport method (opens pool of SMTP connections)
    let smtpTransport = nodemailer.createTransport({
      host: "smtp.qq.com", //qq smtp服务器地址, 如果是其他邮箱需要修改为对应的服务器
      secureConnection: false, //是否使用安全连接,对https协议的
      port: 465, //qq邮件服务所占用的端口
      auth: {
        user: "916707888@qq.com", //开启SMTP的邮箱,有用发送邮件
        pass: "rorvinuqemsybccc" // qq POP3/SMTP授权码,如果是gmail,直接填密码
      }
    });

    // setup e-mail data with unicode symbols
    let mailOptions = {
      from: "guoqzuo <i@zuoguoqing.com>", // sender address
      to: "i@zuoguoqing.com, zuoguoqing@aliyun.com", // list of receivers
      subject: "邮箱验证码", // Subject line
      text: `Hello,您的验证码是 1212323`, // plaintext body
      // html body
      html: `
            Hello,您的验证码是
            <img src="https://iknow-pic.cdn.bcebos.com/adaf2edda3cc7cd96b1584973701213fb80e9140?x-bce-process=image/resize,m_lfit,w_600,h_800,limit_1">
            <img src="http://www.zuo11.com/images/blog/web/qrcode.jpg">

            <div>
              <span style="color:red;">1243</span>
              <div style="margin:50px;border:1px solid #ccc;height: 100px;widht:100px;">1243</div>
            </div>
            <!-- 为ECharts准备一个具备大小(宽高)的Dom -->
            <div id="main" style="width: 600px;height:400px;"></div>
            <script crossorigin="anonymous" integrity="sha384-i+fXrQ+G3+h2478EWpSpIXivtKbbze+0SNOXJGizkAp6DVG/m2fE6hiWeDwskVvp" src="https://lib.baomitu.com/echarts/4.7.0/echarts.js"></script>
            <script type="text/javascript">
                // 基于准备好的dom,初始化echarts实例
                var myChart = echarts.init(document.getElementById('main'));
        
                // 指定图表的配置项和数据
                var option = {
                    title: {
                        text: 'ECharts 入门示例'
                    },
                    tooltip: {},
                    legend: {
                        data:['销量']
                    },
                    xAxis: {
                        data: ["衬衫","羊毛衫","雪纺衫","裤子","高跟鞋","袜子"]
                    },
                    yAxis: {},
                    series: [{
                        name: '销量',
                        type: 'bar',
                        data: [5, 20, 36, 10, 10, 20]
                    }]
                };
        
                // 使用刚指定的配置项和数据显示图表。
                myChart.setOption(option);
            </script>
          `
    }

    // send mail with defined transport object
    smtpTransport.sendMail(mailOptions, function (error, response) {
      if (error) {
        rejected(error);
      } else {
        console.log("Message sent: " + response.message);
        // 发送成功
        console.log('邮件发送成功');
        resolve('发送成功');
      }
      // if you don't want to use this transport object anymore, uncomment following line
      smtpTransport.close(); // shut down the connection pool, no more messages
    });
  });
}

module.exports = sendEmail

上面发送的是html富文本,发送后,可以看到,简单的css样式,图片是可以发送的,但js是无法执行,如下图

send_mail_png.png

# 使用接口生成echarts图片

在上面的例子中,我们知道无法执行js,也就是无法直接在邮箱使用canvas绘图,那么我们可以换个思路,通过一个src,get请求在后端生成echarts图片

怎么在后端用node把echart渲染出来并生成图片呢?这里用到了 node-charts 模块,它使用 puppeteer 来进行渲染截图

/**
 * @description 根据接口生成echarts图片
 * @example
 * GET http://127.0.0.1:9000/png?type=a
 * GET http://127.0.0.1:9000/png?type=b
 */
router.get('/png', async ctx => {
  try {
    let getEchartsPng = require('./echartsPng/index.js')
    let optionsA = require('./echartsPng/optionsA.js')
    let optionsB = require('./echartsPng/optionsB.js')
    let data = await getEchartsPng(ctx.query.type === 'a' ? optionsA : optionsB)
    ctx.set({
      'content-type': 'image/png'
    })
    ctx.body = data
  } catch (e) {
    console.log(e)
    ctx.body = {
      msg: e.message
    }
  }
})

渲染相关逻辑,这样通过接口就可以直接访问一张echart的图片了

const NodeCharts = require('node-charts');

function getEchartsPng(options) {
  return new Promise((resolve, reject) => {
    let nc = new NodeCharts();
    //监听全局异常事件
    nc.on('error', (err) => {
      reject(err)
    });
    nc.render(options, (err, data) => {
      if (err) {
        reject(err)
      }
      resolve(data)
    }, {
      type: 'echarts'
    })
  })
}

module.exports = getEchartsPng

请求截图

node_echarts_img.png

完成demo,参见github: 用邮件发送echarts图表 (opens new window)

上次更新: 2020/10/29 22:59:19