Giter VIP home page Giter VIP logo

blog's People

Contributors

l1uqi avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar

blog's Issues

Electron 自动更新/手动触发更新

背景

我们用Electron开发了桌面应用, 项目同时也在不断更新迭代。我们希望只要发布了最新的版本,用户就能够收到更新提示从而进行升级。调研了市面上的实现方式后决定采取electron-updater插件来实现更新功能。electron-updater只需要简单的文件托管,不需要专用的服务器就能实现更新。

开始

我们先用脚手架新建一个空项目(vue)

vue create electron-vue-demo // 新建项目
vue add electron-builder // 安装electron v11.0.0
npm run electron:serve // 运行项目
npm i electron-updater // 安装electron-updater

配置

publish 发布地址

"build": {
    "productName": "demo",
    "appId": "demo.fspace.com",
    "directories": {
      "output": "release"
    },
    "publish": [
      {
        "provider": "generic",  // 服务器提供商 也可以是GitHub等等
        "url": "http://114.115.142.127:8989/download/", // 更新文件存放位置
        "channel": "latest",
        "useMultipleRangeRequest": false
      }
    ],
}

如果是vue-cli-plugin-electron-builder打包则会报错如下:

Question||'build' in the application package.json is not supported since 3.0

因为3.0后不支持json的方式, 需要移除package.json “build”

vue.config.js 添加builderOptions
后续需要在vue中使用ipcRenderer(主进程与渲染进程通信)
所以需要设置
// nodeIntegration: true

module.exports = {
	...
  pluginOptions: {
    electronBuilder: {
    	nodeIntegration: true, // ipcRenderer
      builderOptions: {
        productName: "demo",
        appId: "demo.fspace.com",
        directories: {
          "output": "release"
        },
        publish: [
          {
            "provider": "generic",  // 服务器提供商 也可以是GitHub等等
            "url": "http://localhost:3006/", // 更新文件存放位置
            "channel": "latest",
            "useMultipleRangeRequest": false
          }
        ]
      }
    }
  }
}

background.js
初始化 autoUpdater

'use strict'

import { app, protocol, BrowserWindow, ipcMain } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
import * as path from 'path';
const fs = require('fs');
const { autoUpdater } = require('electron-updater');

const isDevelopment = process.env.NODE_ENV !== 'production';
const DOWNLOAD_URL = 'http://localhost:3006/';

var package_json = require('../package.json');
var mainWindow = null;


// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
  { scheme: 'app', privileges: { secure: true, standard: true } }
])

async function createWindow() {
  // Create the browser window.
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      
      // Use pluginOptions.nodeIntegration, leave this alone
      // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
      nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION
    }
  })

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    // Load the url of the dev server if in development mode
    await mainWindow.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
    if (!process.env.IS_TEST) mainWindow.webContents.openDevTools()
  } else {
    createProtocol('app')
    // Load the index.html when not in development
    mainWindow.loadURL('app://./index.html')
  }
}

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (BrowserWindow.getAllWindows().length === 0) createWindow()
})

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
  if (isDevelopment && !process.env.IS_TEST) {
    // Install Vue Devtools
    try {
      await installExtension(VUEJS_DEVTOOLS)
    } catch (e) {
      console.error('Vue Devtools failed to install:', e.toString())
    }
  }
  console.log('ready')
  createWindow()
  updateHandle();
})

// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
  if (process.platform === 'win32') {
    process.on('message', (data) => {
      if (data === 'graceful-exit') {
        app.quit()
      }
    })
  } else {
    process.on('SIGTERM', () => {
      app.quit()
    })
  }
}


function updateHandle() {
  autoUpdater.currentVersion = package_json.version;

  autoUpdater.setFeedURL(DOWNLOAD_URL);

  // 取消自动更新
  autoUpdater.autoDownload = false;
  
  autoUpdater.on('checking-for-update', (info) => {
    // 开始检查是否有新版本
    // 可以在这里提醒用户正在查找新版本
    console.log('checking-for-update')
  })

  autoUpdater.on('update-available', (info) => {
    // 检查到有新版本
    // 提醒用户已经找到了新版本
    console.log('检查到有新版本')
  })
  
  autoUpdater.on('error', (err) => {
    // 自动升级遇到错误
  })
  
}

打包测试

package.json

版本号 1.0.1

{
  "name": "electron-vue-demo",
  "version": "1.0.1",
  ...
}

执行打包

vue-cli-service electron:build

打包后release目录 (当前为mac打包)

├── release
│   ├── demo-1.0.1-mac.zip
│   ├── demo-1.0.1.dmg 					 // 安装文件
│   ├── demo-1.0.1.dmg.blockmap // 用于差异更新, mac好像无效 
│   ├── latest-mac.yml 					// 更新相关文件
│   └── mac
├── ...
└── package.json

搭建静态服务

这里使用koa koa-static 配置静态目录

├── server
│   ├── public  // 存放更新文件			
│   └── server.js 	
├── ...
└── package.json

我们把demo-1.0.1-mac.zip / latest-mac.yml / 更新日志 放入更新目录public

{
  "version": "V1.0.1",
  "content": [
  "-🎉  v1.0.1版本盛大发布。"
  ]
}

server.js

const Koa = require('koa');
const app = new Koa();
const path = require('path');
const serve = require('koa-static');
 
const main = serve(path.join(__dirname+'/public'));
app.use(main);
 
app.listen(3006,function(){
  console.log("监听3006端口")
});

回到项目

background.js

import { ipcMain } from 'electron'

// ipcMain 监听渲染进程checkForUpdate 事件
ipcMain.on("checkForUpdate",() => {
  autoUpdater.currentVersion = package_json.version;
  //执行更新检查
  autoUpdater.checkForUpdates();
})

function updateHandle() {
	...
  autoUpdater.on('update-available', (info) => {
    // 检查到有新版本
    // 提醒用户已经找到了新版本
    console.log('检查到有新版本', info)
  })
  ...
}

app.vue

<template>
  <div id="app">
    <button @click="checkForUpdates">检查更新</button>
  </div>
</template>
<script>
import { ipcRenderer } from "electron";
export default {
  name: 'App',
  methods: {
    checkForUpdates() {
      // 通知主进程检查更新
      ipcRenderer.send('checkForUpdate')
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

点击按钮, 控制台打印如下

检查到有新版本 {
  version: '1.0.1',
  files: [
    {
      url: 'demo-1.0.1-mac.zip',
      sha512: 'PJeIr6HilOlNrcR8HEimQQuJHjEiK7x2PHhOGnmul5tTI2n0R7+6PP8S5j3+bwfZzSkjBWWWYnlR8WNoQ17YBQ==',
      size: 77708593,
      blockMapSize: 82077
    },
    {
      url: 'demo-1.0.1.dmg',
      sha512: 'i++/bWJ7pxIkShS+WehKkP8rLMjbKtHvFV/aLmDDj8lEqeyKP8cnVpSSlNNbqOwcqbxSzR5t07QMIUIVf0AMYw==',
      size: 80015179
    }
  ],
  path: 'demo-1.0.1-mac.zip',
  sha512: 'PJeIr6HilOlNrcR8HEimQQuJHjEiK7x2PHhOGnmul5tTI2n0R7+6PP8S5j3+bwfZzSkjBWWWYnlR8WNoQ17YBQ==',
  releaseDate: '2021-04-21T05:38:20.929Z'
}

autoUpdater.downloadUpdate(); // 下载更新
autoUpdater.quitAndInstall(); // 执行推出安装更新
依次执行后实现了更新操作, 当然这对用户来说非常不友好,需要把更新流程交给用户去控制。

autoUpdater给我们提供 download-progress(更新进度)、update-downloaded(更新完成) 监听。

app.vue

<template>
  <div id="app">
    <a-button @click="checkForUpdates">检查更新</a-button>
    <!-- 更新提示框 -->
    <div class="main-container__upgrade-panel" v-if="show">
      <div class="main-container__upgrade-panel-title">
        {{`发现新版本${versionInfo.version}`}}
        <span @click="() => { show = !show }"><a-tooltip title="最小化" placement="top"><a-icon type="down-circle" /></a-tooltip></span>
      </div>
      <div class="main-container__upgrade-panel-body">
        <div class="main-container__pd1t">
          更新日志:
          </div>
          <div v-for="(item, index) in versionInfo.content" :key="index">{{item}}</div>
      </div>
      <div class="main-container__upgrade-panel-footer">
        <div style="width: 305px;">
          <a-progress
            :stroke-color="{
              from: '#108ee9',
              to: '#87d068',
            }"
            :percent="progress.percent"
            status="active"
          />
        </div>
        <a-button style="margin-right: 10px;" v-if="canInstall" type="primary" @click="() => icpSend('quitAndInstall')">安装</a-button>

        <a-button style="margin-right: 10px;" v-else type="primary" :loading="loading"  @click="() => { loading = true, icpSend('downloadUpdate') }"> <a-icon v-if="!loading" type="down-square" /> 更新</a-button>

        <a-button :disabled="progress.percent > 0" type="dashed" @click="() => { show = !show }">下次再说</a-button>

      </div>
    </div>
    
  </div>
</template>
<script>
import { ipcRenderer } from "electron";
export default {
  name: 'App',
  data() {
    return {
      DOWNLOAD_URL: 'http://localhost:3006/',
      canInstall: false,
      show: false,
      progress: {
        bytesPerSecond: 0,
        delta: 0,
        percent: 0,
        total: 0,
        transferred: 0
      },
      loading: false,
      versionInfo: {
        version: '',
        content: [
          '123',
          '456'
        ]
      }
    }
  },
  created() {
    // 版本有更新时提示
    ipcRenderer.on("updateAvailable", async (event, info) => {
      const verInfo = await this.getVersionInfo(info);
      if (verInfo) {
        try {
          this.versionInfo.version = JSON.parse(verInfo).version;
          this.versionInfo.content = JSON.parse(verInfo).content;
        } catch (e) {
          console.log(e)
        }
        this.show = true;
      }

    });
    // 下载进度条
    ipcRenderer.on("downloadProgress", (event, progressObj) => {
      progressObj.percent = Number(progressObj.percent.toFixed(1));
      this.progress = {
        ...progressObj
      };
    });

    ipcRenderer.on("isUpdateNow", () => {
      this.canInstall = true;
      this.show = true;
    });
  },
  methods: {
    async getVersionInfo(info) {
      return new Promise((resolve) => {
        let xhr = new XMLHttpRequest();
        xhr.open('get', this.DOWNLOAD_URL + info.version + '.json', true);
        xhr.send(null);
        xhr.onreadystatechange = function () {
          
          if (xhr.readyState == 4) {
            if (xhr.status == 200) {
              
              resolve(xhr.responseText)
            } else {
              resolve(null)
            }
          }
        };

      });
    },
    icpSend(name) {
      ipcRenderer.send(name);
    },
    checkForUpdates() {
      ipcRenderer.send('checkForUpdate')
    }
  }
}
</script>

<style lang="scss">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  margin-top: 60px;
}

.main-container {
  width: 100%;
  position: relative;
  // display: flex;
  background: #f7f7f7;

  &__drag {
    position: absolute;
    width: calc(100% - 100px);
    height: 25px;
    -webkit-app-region: drag;
    .overlay {
      pointer-events: none;
    }
  }

  &__controls {
    position: absolute;
    right: 0;
    -webkit-app-region: no-drag;
    top: 0;
    z-index: 200;
    border-radius: 0 0 3px 3px;
    padding: 0;
    background: #bfbfbf21;
    :hover {
      color: white;
      background: gray;
    }
    :nth-child(3):hover{
      background-color: red;
    }

    &-item {
      display: inline-block;
      padding: 5px 10px;
      color: #ccc;
      font-size: 12px;
      -webkit-app-region: no-drag;
    }
  }

  &__upgrade-panel {
    position: fixed;
    z-index: 9999;
    right: 10px;
    bottom: 25px;
    width: 340px;
    background-color: #34373c;
    color: white;
    border-radius: 3px;
    font-size: 12px;
    box-shadow: 0px 0px 5px 5px rgba(133,133,133,0.25);

    ::-webkit-scrollbar {
      display: none; /* Chrome Safari */
    }

    &-title {
      padding: 10px 15px;
      width: 100%;
      height: 40px;
      border-bottom: 1px solid white;

      span {
        position: absolute;
        font-size: 14px;
        right: 10px;
      }

      span:hover {
        color:#FFFFFF;
        background-color:#6dd214;
        text-shadow:none;
      }
    }

    &-body {
      overflow-y: auto;
      padding: 10px 15px;
      max-height: 100px;
    }
    &-footer {
      padding-left: 10px;
      padding-bottom: 10px;

      a-button {
        margin-right: 15px;
      }
    }
  }

  &__pd1t {
    padding-top: 5px;
  }
}

.ant-progress-text {
  color: white !important;
}
</style>

background.js

'use strict'

import { app, protocol, BrowserWindow, ipcMain } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
const fs = require('fs');
const { autoUpdater } = require('electron-updater');

const isDevelopment = process.env.NODE_ENV !== 'production';
const DOWNLOAD_URL = 'http://localhost:3006/';

var package_json = require('../package.json');
var mainWindow = null;


// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
  { scheme: 'app', privileges: { secure: true, standard: true } }
])

async function createWindow() {
  // Create the browser window.
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      
      // Use pluginOptions.nodeIntegration, leave this alone
      // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
      nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION
    }
  })

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    // Load the url of the dev server if in development mode
    await mainWindow.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
    if (!process.env.IS_TEST) mainWindow.webContents.openDevTools()
  } else {
    createProtocol('app')
    // Load the index.html when not in development
    mainWindow.loadURL('app://./index.html')
  }
}

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (BrowserWindow.getAllWindows().length === 0) createWindow()
})

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
  if (isDevelopment && !process.env.IS_TEST) {
    // Install Vue Devtools
    try {
      await installExtension(VUEJS_DEVTOOLS)
    } catch (e) {
      console.error('Vue Devtools failed to install:', e.toString())
    }
  }
  console.log('ready')
  createWindow()
  updateHandle();
})

// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
  if (process.platform === 'win32') {
    process.on('message', (data) => {
      if (data === 'graceful-exit') {
        app.quit()
      }
    })
  } else {
    process.on('SIGTERM', () => {
      app.quit()
    })
  }
}


const deleteFile = (path) => {
  var files = [];
  if( fs.existsSync(path) ) {
      files = fs.readdirSync(path);
      files.forEach(function(file){
          var curPath = path + "/" + file;
          if(fs.statSync(curPath).isDirectory()) {
              deleteFile(curPath);
          } else {
              fs.unlinkSync(curPath);
          }
      });
      fs.rmdirSync(path);
  }
};

function updateHandle() {
  autoUpdater.currentVersion = package_json.version;

  autoUpdater.setFeedURL(DOWNLOAD_URL);

  // 取消自动更新
  autoUpdater.autoDownload = false;
  
  autoUpdater.on('checking-for-update', (info) => {
    // 开始检查是否有新版本
    // 可以在这里提醒用户正在查找新版本
  })

  autoUpdater.on('update-available', (info) => {
    // 检查到有新版本
    // 提醒用户已经找到了新版本
    console.log(info)
    mainWindow.webContents.send('updateAvailable', info)
  })
  
  autoUpdater.on('update-not-available', (info) => {
    // 检查到无新版本
    // 提醒用户当前版本已经是最新版,无需更新
  })

  autoUpdater.on('download-progress', function (progressObj) {
    // 更新进度条
    mainWindow.webContents.send('downloadProgress', progressObj)
  })
  
  autoUpdater.on('error', (err) => {
    // 自动升级遇到错误
  })
  
  autoUpdater.on('update-downloaded', (ev, releaseNotes, releaseName) => {
    // 自动升级下载完成
    // 可以询问用户是否重启应用更新,用户如果同意就可以执行 autoUpdater.quitAndInstall()
    mainWindow.webContents.send('isUpdateNow')
  })
}

ipcMain.on("checkForUpdate",() => {
  console.log(autoUpdater.currentVersion)
  autoUpdater.currentVersion = package_json.version;
  //执行自动更新检查
  autoUpdater.checkForUpdates();
})

ipcMain.on("downloadUpdate",() => {
  try {
    // 更新前删除本地更新包
    deleteFile(autoUpdater.app.baseCachePath)
  }catch {
    
  }
  //执行自动更新检查
  autoUpdater.downloadUpdate();
})

ipcMain.on("quitAndInstall",() => {
  //执行自动更新检查
  autoUpdater.quitAndInstall();
})

最终效果

preview

ESLint 如何在Vue或React中使用?

vue或react都有对应的插件实现

eslint-plugin-vue

eslint-plugin-react

由于 Vue 中的单个文件组件不是普通的 JavaScript,因此无法使用默认解析器,因此引入了新的文件组件。 通过表示模板语法的特定部分以及标签内的内容生成增强的 AST 节点。

以上面Vue文件报错为例子

我们可以通过eslint-plugin-vue来实现

首先项目根目录安装

npm install --save-dev eslint eslint-plugin-vue

然后执行 npm run eslint-fix进行校验

前面所遇到的html标记已经得到解决 eslint能正确识别vue文件。

配合IDE食用更佳

在实际的开发过程中, 当然希望编辑器能即使反馈错误并提示

以VsCode为例

安装插件

1

预期效果

2

如果未生效

请确保EsLint开启

如遇报错 解决报错直至开启

3

CSS 通过子元素设置父元素

参考资料:

CSS 父元素如何通过判断子元素来设置样式

技术周刊 2021-05-21:Canvas 之春

背景

某些特定场景下 我们希望通过子元素来判断从而修改父级属性。
除了JS控制之外是否有CSS实现这方面功能
查了大量资料发现css现阶段没有实现这块

原因如下:

由于它违反了CSS目前的“不回溯”的原则,因此一直没有被纳入规范

其实你从浏览器的角度来考虑这件事情,便不会有疑问了。假如浏览器支持父级或者前面兄弟元素选择器的话,就不得不在渲染到当前元素时,倒回去找到特定的元素,重新渲染。因为选择器是可以组合的,甚至还会造成很多其它元素也需要重新渲染(想象.someClass:parent div),这样带来的reflow和repaint会造成难以估量的性能问题,甚至计算盒模型的栈保不齐都要溢出。所以自然不会有类似的选择器出现了。

现在

Chrome 意图实验性支持 CSS :has() 选择器,可以用来选择父级元素。目前 Igalia 公司正在为 Chrome 实现该选择器,其团队成员 Brian Kardell 还发表了博文 Can I :has() 进行了详细介绍。

<style>
.parent:has(.red) { color: red } /* 将匹配 .red 的父元素 .parent */
.parent:has(.green) { color: green } /* 将匹配 .green 的父元素 .parent */
</style>
<div class="parent"> <!-- color: red -->
  <div class="red"></div>
</div>
<div class="parent"> <!-- color: green -->
  <div class="green"></div>
</div>

1

结论

还是用js吧~

向往的生活

身边有几个走不散的知心朋友

真正关心你的家人

温暖的家

这就是我向往的生活

Luckysheet 初体验

介绍

Luckysheet ,一款纯前端类似excel的在线表格,功能强大、配置简单、完全开源。

开始

下载vue示例

git clone https://github.com/hjwforever/luckysheet-vue3-vite.git
// 安装依赖
npm install 
// 启动
npm run dev

运行后如下:
1

文件目录:

├── src
│   └── assets
|   └── components
|       └── LuckySheet.vue 
├── ...
└── package.json

打开 LuckySheet.vue

为了看起来更简洁 移除暂时用不到的方法

<template>
  <div id="luckysheet"></div>
</template>

<script setup>
import {onMounted } from 'vue'

onMounted(() => {
  // 创建luckysheet 表格
  luckysheet.create({
    container: 'luckysheet'
  })
})
</script>

<style  scoped>
#luckysheet {
  margin: 0px;
  padding: 0px;
  position: absolute;
  width: 100%;
  left: 0px;
  top: 30px;
  bottom: 0px;
}
</style>

下载源码

版本: "2.1.13"

git clone https://github.com/mengshukeji/Luckysheet.git
// 安装依赖
npm install 
// 启动
npm run dev
├── docs // vuepress 文档
├── src
|   |── assets
|   |── controllers
|   |── css
|   |── data
|   └── ...
├── config.js // 表格配置文件
├── core.js // 入口
├── index.html 
├── index.js
└── package.json

ESLint 初体验

介绍

ESLint是一个用来识别 ECMAScript 并且按照规则给出报告的代码检测工具,使用它可以避免低级错误和统一代码的风格。如果每次在代码提交之前都进行一次eslint代码检查,就不会因为某个字段未定义为undefined或null这样的错误而导致服务崩溃,可以有效的控制项目代码的质量

好处

俗话说的好没有规矩不成方圆 代码规范是程序员的根本,入门第一步,从规范代码开始。
eslint根本目的是统一项目的代码风格规范,养成规范的写代码习惯,减少不必要的错误和隐患。

安装

    npm install -g eslint // 全局安装

    npm install --save-dev eslint // 项目安装

使用

项目根目录新建.eslintrc.js文件 配置eslint。

如果全局安装eslint可以在项目运行, 初始化配置文件。

可以根据个人喜好或团队选择

eslint --init

1 How would you like to use ESLint? (Use arrow keys)
To check syntax only   // 只检查语法
❯ To check syntax and find problems // 检查语法并找出问题
To check syntax, find problems, and enforce code style // 检查语法、发现问题并强制执行代码样式

2 What type of modules does your project use? (Use arrow keys)
❯ JavaScript modules (import/export) 
  CommonJS (require/exports) 
  None of these

3 Which framework does your project use? 
  React 
  Vue.js 
❯ None of these

...
  • 初始化配置如下
module.exports = {
    "env": {
        "browser": true,
        "es6": true
    },
    "extends": "eslint:recommended",
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    "parserOptions": {
        "ecmaVersion": 2018
    },
    "rules": {
    }
};
  • 接下来添加一条rule
"rules": {
	// if while function 后面的{ 必须与if在同一行。
	"brace-style": [2, "1tbs", { "allowSingleLine": true }],
}
  • 测试文件: util.js
var d = new Date()
var time = d.getHours()
if (time<10)
{
document.write("<b>Good morning</b>" )
}
else if (time>=10 && time<16) 
{
document.write("<b>Good day</b>")
}
else
{
document.write("<b>Hello World!</b>")
}
  • 控制台执行
eslint src/util.js


...src/util.js

4:1  error  Opening curly brace does not appear on the same line as controlling statement  brace-style
   6:1  error  Closing curly brace does not appear on the same line as the subsequent block   brace-style
   9:1  error  Closing curly brace does not appear on the same line as the subsequent block   brace-style
  11:1  error  Opening curly brace does not appear on the same line as controlling statement  brace-style

✖ 4 problems (4 errors, 0 warnings)
  4 errors and 0 warnings potentially fixable with the `--fix` option.

eslint rule生效 结果自然是会报错
在当前场景大括号不能单独占一行

这时我们可以执行 eslint --fix
package中配置命令替代单个eslint --fix

eslint --fix util.js // 自行纠正

// 配置命令全局执行,省事
// package.js 
"scripts": {
	"eslint-fix": "eslint src/**/*.*  --fix"
}

接下来在新建一个vue文件
src/index.vue

控制台执行

npm run eslint-fix
出现报错

::: warning
eslint此时不认识vue文件, 所以只能当成js文件进行处理,

正因为js外层不能包含html标记 所以会出现报错信息。
:::

使用Lottie 让动画更简单

介绍

Lottie 是Airbnb推出Library 它可将After Effects动画经由Bodymovi扩展插件输入成一个JSON动画文件格式, 适用于 Web、iOS、Android、Windows、QT、Tizen 和其他平台。
lottie_1

使用

如果你有After Effects动画, 可通过AE插件Bodymovi 生成JSON动画

没有After Effects动画可以让设计来制作, 或者学习制作~

当然也可以使用 Lottie Files 提供的免费动画

lottie_2

接着从Lottie Files下载一个动画任意动画

Flutter中使用

  1. lottie json 放入项目assets 目录下
  2. 安装 lottie
dependencies:
  lottie: ^1.3.0 
  1. 使用
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
          body: Center(
        child: Container(child: Lottie.asset('assets/lottie/animation.json')),
      )),
    );
  }
}

lottie_3

我们可以使用AnimationController 控制动画

  • forward() 启动
  • stop() 停止
  • reverse() 反向播放
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';

void main() => runApp(LottieScreen());

class LottieScreen extends StatefulWidget {
  LottieScreen({Key? key}) : super(key: key);
  @override
  State<StatefulWidget> createState() => _LottieScreenState();
}

class _LottieScreenState extends State<LottieScreen>
    with SingleTickerProviderStateMixin {
  late AnimationController lottieController;

  @override
  void initState() {
    super.initState();

    lottieController = AnimationController(
      vsync: this,
    );

    lottieController.addStatusListener((status) async {
      if (status == AnimationStatus.completed) {
        lottieController.reset();
      }
    });
  }

  @override
  void dispose() {
    lottieController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
          body: Center(
        child: Column(children: [
          Lottie.asset("assets/lottie/animation.json",
              repeat: false,
              controller: lottieController, onLoaded: (composition) {
            lottieController.duration = composition.duration;
            lottieController.forward();
          }),
          const SizedBox(
            height: 24,
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              RaisedButton(
                onPressed: () {
                  lottieController.forward();
                },
                child: Text("启动"),
              ),
              RaisedButton(
                onPressed: () {
                  lottieController.stop();
                },
                child: Text("停止"),
              ),
              RaisedButton(
                onPressed: () {
                  lottieController.reverse();
                },
                child: Text("反向播放"),
              ),
            ],
          )
        ]),
      )),
    );
  }
}

lottie_4

Vue中使用

Electron 启动动画实现

背景

Electron是通过Chromium和Node.js集成来达到这一开发体验的, 我们可以用React / Vue 很轻松的搭建一个桌面应用程序。应用启动时就避免不了短暂的白屏或者需要启动时加载基础数据。

那么怎样解决这一问题呢?

有APP开发经验的同学肯定立马就想到了 启动动画(开屏广告)。

思路

  1. 应用启动额外创建 Loading窗口 ,并且主窗口默认隐藏
  2. 主窗口加载完毕通过 ipcRenderer 通知主窗口显示, Loading窗口关闭

实现

  • 准备开屏动画

  • 配置主进程main.js文件

const createWindow = async () => {
  mainWindow = new BrowserWindow({
    minHeight: 600,
    minWidth: 1024, width: 1280, height: 720, titleBarStyle: 'hidden', frame: false, show: false
  });
    ……
};
  • 创建loading窗口
// loading
const createLoadingWindow = async () => {   
  loadingWindow = new BrowserWindow({
    height: 260,
    width: 650,
    show: true,
    transparent: true,  // 透明窗口
    maximizable: false,  //禁止双击放大
    frame: false   // 去掉顶部操作栏
  })

  loadingWindow.loadURL(url.format({
    // loading.html 加载动画
    pathname: path.join(__dirname, './lib/loading/loading.html'),
    protocol: 'file:',
    slashes: true
  }))
    ……
}
  • 动画展示隐藏
import { ipcRenderer } from "electron";
const ipcMain = require('electron').ipcMain;

// 页面加载完毕时调用 通知关闭loading
ipcRenderer.send("close-loading-window");

app.on('ready', () => {
  // 创建加载动画 
  createLoadingWindow();
  // 创建主窗口
  createWindow();
  // 监听页面加载完毕事件
  ipcMain.on('close-loading-window', () => {
    
    if(loadingWindow) {
      loadingWindow.close();
    }
    mainWindow.show();
  })

});

完成

animation

不足

transparent: true, // 透明窗口

在win7下不起作用

解决方案: 禁用硬件加速(未尝试)

app.disableHardwareAcceleration();

app.on('ready', () => {
  setTimeout(() => {
    createWindow();
    autoUpdater.checkForUpdatesAndNotify();
    createMenu();
  }, 50);
});

心之所想

刚渡过了我的27岁生日

回顾这几年 似乎过的还不错

有坚持看书提升 也培养几个比较有意义的兴趣爱好

工作上 有互相帮助的同事 工作轻松 工资尚可

烟、槟榔这些不好的习惯也早戒了

遗憾的是晨跑的确是没能坚持下(尽力了... 有想互相监督的可以找我 )

身边的朋友也一直都在

有旧识 新识

无需敷衍,无需讨好

偶尔撸串 喝几杯 畅聊人生

真是再好不过了

谢谢你们


到了这个年纪 有个始终都绕不开的那便是情感这个话题

随着年龄的增长 身边结婚、有小孩的朋友是越来越多了

其实我真的是羡慕 , 羡慕他们的生活 当然身边也有婚后过的不是那么幸福的朋友啦

就如《围城》褚慎明说英国有句古话:

::: tip
“ 结婚仿佛金漆的鸟笼,笼子外面的鸟想住进去,笼内的鸟想飞出来;所以结而离,离而结,没有了局。”
:::

渐渐地我也接受了相亲(以前的我肯定瞧不起相亲, 最终小丑竟是我自己! )

遇见了各种各样的女生, 至于到现在都没个结果 根源肯定是我自己

经历了几次用尽全力最终却得到背叛 热情消耗殆尽且放一边 更多的是丢失了自信心

朋友也有介绍优秀的女生给我, 用他们的话说便是 她哪点不如她 人家还对你那么好

每每我都是简单的一句 不喜欢 敷衍了事

真的是不喜欢嘛, 真要形容的话 一句歌词再好不过了

“得不到的永远在*动,被偏爱的都有恃无恐”

好像从来便是如此, 正因此伤害过那些真正对我好的人, 好在她们现在都过的很好啦!

有时候想想这便是因果吧

愿大家
无所畏惧漫漫人生路,进取高唱潇潇岁月歌。

Flutter 启动页实现

前言

应用开启时, 会出现短暂白屏才会出现加载后页面, 给用户的感觉也不好

原生

...

Pub 插件

这里使用flutter_native_splash

  1. 安装依赖
dependencies:
  flutter:
    sdk: flutter
  ...
  flutter_native_splash: ^2.1.6
  1. 设置 pubspec.yaml
  • image: 图片
  • color: 背景颜色 用于图片不能充满屏幕
  • android: true 生成 andorid
  • ios: true 生成 ios
  • android_gravity: "fill" // 铺满
  • ios_content_mode: "scaleAspectFit" // 铺满
  • ...
flutter_native_splash:
  image: assets/images/splash.png
  color: "#ffffff"
  android: true
  ios: true
  android_gravity: "fill"
  ios_content_mode: "scaleAspectFit"
  1. 生成启动页
flutter pub pub run flutter_native_splash:create
[Android] Creating splash images
[Android] Creating dark mode splash images
[Android] Updating launch background(s) with splash image path...
[Android]  - android/app/src/main/res/drawable/launch_background.xml
[Android]  - android/app/src/main/res/drawable-v21/launch_background.xml
[Android] Updating styles...
[Android]  - android/app/src/main/res/values-v31/styles.xml
[Android] No android/app/src/main/res/values-v31/styles.xml found in your Android project
[Android] Creating android/app/src/main/res/values-v31/styles.xml and adding it to your Android project
[Android]  - android/app/src/main/res/values/styles.xml
[iOS] Creating images
[iOS] Creating dark mode images
[iOS] Updating LaunchScreen.storyboard with width, and height
[iOS] Updating ios/Runner/Info.plist for status bar hidden/visible
Web folder not found, skipping web splash update...
╔════════════════════════════════════════════════════════════════════════════╗
║                                 WHAT IS NEW:                               ║
╠════════════════════════════════════════════════════════════════════════════╣
║ You can now keep the splash screen up while your app initializes!          ║
║ No need for a secondary splash screen anymore. Just use the remove()       ║
║ method to remove the splash screen after your initialization is complete.  ║
║ Check the docs for more info.                                              ║
╚════════════════════════════════════════════════════════════════════════════╝

✅ Native splash complete.
Now go finish building something awesome! 💪 You rock! 🤘🤩
Like the package? Please give it a 👍 here: https://pub.dev/packages/flutter_native_splash

预览

1

当然flutter_native_splash也提供了方法让我们主动调用关闭,我们可以初始化数据等耗时操作执行完主动关闭

void main() async {
  WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
  // 启动页设置手动关闭
  FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
  runApp(App());
  // 模拟初始化数据等待5s
  await Future.delayed(Duration(seconds: 3));
  // 启动页设置关闭
  FlutterNativeSplash.remove();
}

2

完结

如果遇到改状态栏等 就需要原生解决了~

简单需求flutter_native_splash还是能满足的

Flutter Andoird 返回键退出友好处理

背景

Android手机会有虚拟按键, 或者实体按键。 用户点击返回按键或误触时 如果刚打开或者路由栈没有更多页面则会退出应用
直接退出应用的话 用户体验就太差了, 所以大多数的做法是在一定时间内再按返回才进行退出操作

解决

在Flutter中我们可以通过 WillPopScope 来实现返回按钮拦截

const WillPopScope({
  ...
  required WillPopCallback onWillPop,
  required Widget child
})

onWillPop是回调函数, 当用户点击返回按钮时调用

import 'package:flutter/material.dart';
import 'package:get/get.dart';

class WillPopTest extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => WillPopTestState();
}

class WillPopTestState extends State<WillPopTest> {
  DateTime? _lastClickTime; //上次点击时间

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return GetMaterialApp(
      home: Scaffold(
        body: WillPopScope(
            onWillPop: () async {
              if (_lastClickTime == null ||
                  DateTime.now().difference(_lastClickTime!) >
                      Duration(seconds: 1)) {
                _lastClickTime = DateTime.now();
                Get.snackbar('提示', '再按一次退出',
                    margin: EdgeInsets.only(bottom: 10),
                    snackPosition: SnackPosition.BOTTOM);
                return false;
              }
              return true;
            },
            child: Container(
              child: Center(
                child: Text('点击两次返回退出app'),
              ),
            )),
      ),
    );
  }
}

1

前端曝光实现


title: 前端曝光实现
top_img: false
date: 2022-11-08
categories:

  • 前端
    tags:
  • 埋点

什么是曝光?

商品曝光人数:看到商品在首页、列表页、活动页面,以及在商品详情页下方的更多展现的人数。(不包括商品详情页的访客数量)
商品曝光次数:商品在店铺首页、列表页、活动页面,以及在商品详情页下方的更多展现的次数。(不包括商品详情页的浏览量)

通过商品曝光我们能得出商品在不同营销位的比重, 从而得出用户操作喜好

实现

曝光肯定是结合埋点一起使用, 通过采集某个商品是否出现在用户的可视区域内, 进行上报。

判断是否在用户可视区域内

1. getBoundingClientRect()

getBoundingClientRect 方法返回一个 对象,该 DOMRect 对象提供有关元素大小及其相对于视口的位置的信息。

目标元素 rect 满足 top > 0 && left > 0 && bottom >= 视窗高度 && right <= 视窗宽度
便能得出元素完全在视窗内。 在长列表下 我们可以通过监听滚动条事件, 从而获取目标元素是否暴露在用户视窗内。

这种方法实现起来简单,兼容性相对较好,当监听的元素过多时,会造成性能问题,出现卡顿,影响使用体验。

<html>

<head>
    <title>getBoundingClientRect</title>
</head>

<body>
    <style>
        body {
            margin: 0;
        }

        .rect {
            margin-top: 50px;
            width: 100%;
            height: 200px;
            background: red;
            text-align: center;
            line-height: 200px;
        }
    </style>
    <div id="contents" style="width: 100%"></div>
    <script>
        // 最大渲染条数
        const renderNumber = 100;

        let i = 0;
        // 渲染列表
        const renderList = [];

        const contents = document.getElementById("contents");

        // 已曝光
        let inVpList = [];

        const unionArray = (arr1, arr2) => {
            const newSet = new Set(arr1)
            arr2.map(item => {
                newSet.add(item)
            })
            return [...newSet].sort((a, b) => {
                return a - b;
            })
        }
        // 节流
        const throttled = (fn, delay = 500) => {
            let timer = null
            let starttime = Date.now()
            return function () {
                let curTime = Date.now() // 当前时间
                let remaining = delay - (curTime - starttime)  // 从上一次到现在,还剩下多少多余时间
                let context = this
                let args = arguments
                clearTimeout(timer)
                if (remaining <= 0) {
                    fn.apply(context, args)
                    starttime = Date.now()
                } else {
                    timer = setTimeout(fn, remaining);
                }
            }
        }

        const createElement = (number) => {
            let div = document.createElement('div');
            let textNode = document.createTextNode(number);
            div.appendChild(textNode);
            div.style.color = "white";
            div.style.fontSize = "100px";
            div.style.fontWeight = 600;
            div.className = 'rect'
            div.setAttribute("id", `div_${number}`);
            contents.appendChild(div);
            return div;
        }

        // 判断可视区域内
        const isElementInViewport = (el) => {
            let rect = el.getBoundingClientRect();
            return (
                rect.top >= 0 &&
                rect.left >= 0 &&
                rect.bottom <=
                (window.innerHeight || document.documentElement.clientHeight) &&
                rect.right <= (window.innerWidth || document.documentElement.clientWidth)
            );
        };

        while (i < renderNumber) {
            renderList.push(createElement(i));
            i++;
        }

        const addInVpList = (arr) => {
            return unionArray(inVpList, arr);
        }


        const handleScrollChange = (e) => {
            const list = renderList.filter(item => isElementInViewport(item)).map(item => item.textContent);
            inVpList = addInVpList(list)

            console.log(`已曝光: ${inVpList}`)
        }

        document.addEventListener('scroll', throttled(handleScrollChange))

    </script>
</body>

</html>

前端业务埋点SDK实践


title: 前端业务埋点SDK实践
date: 2022-11-07
categories:

  • 前端
    tags:
  • 埋点采集

参考资料:

腾讯二面:现在要你实现一个埋点监控 SDK,你会怎么设计?
为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?

最近项目需要采集用户的行为数据, 从而进行分析得到页面点击量、访问量、访问路径等重要数据, 为运营和业务人员提供精准数据,为产品优化和精细化运营提供数据支持。

111

埋点方式选择

这里采用代码侵入式埋点的方式进行、 SDK 提供 点击事件、曝光事件、页面时长进行上报

  • 点击事件(用户每点击一次按钮、商品、区块.. 就记录一次数据)
  • 曝光事件(专区、活动、商品...用户可见)
  • 页面时长(活动、页面 停留时间)

埋点方式实现

点击事件

如何让前端同事更轻松的捕获数据、这里实现了 dom 自定义事件实现自动上报, 在 vue 中也提供了指令 如有特殊业务场景也可以使用手动进行上报。

// 自动上报
// js

// vue
<button v-track:click="{
  'page_id': 1,
  'event_type': 12, // 事件类型
  'objs' // 业务数据
  }"
/>

<button v-track:keyup="{
  'page_id': 2,
  'event_type': 12, // 事件类型
  'objs' // 业务数据
  }"
/>

// 手动上报
trakerSDK.sendTracker({
  'page_id': 3,
  'event_type': 12,
  'object_ids': [1, 2, 3]
});

页面时长

页面时长统计, 我们可以用时间戳来算出用户停留的页面时长。
页面离开 - 页面进入 = 停留时长。

// 单页应用
router.beforeEach((to, from, next) => {
  // 获取当前时间
  const timestamp = new Date().getTime();
  // 上次时间
  const pretimestamp = getCache(LocalStoreEnum.PRE_TIMESTAMP);
  // 存入当前时间
  setCache(LocalStoreEnum.PRE_TIMESTAMP, timestamp);

  // 停留时间(s)
  let secound = (timestamp - pretimestamp) / 1000;
  next();
});

这样的话大多数场景都能够满足, 但是还有特殊场景无法满足。
如 PC 端 浏览器 Tab 选项卡切换、 APP 切换应用等。
这时候就需要另外的方案, Page Visibility API 能够解决这个问题

document.addEventListener("visibilitychange", () => {
  const state = document.visibilityState;
  let callbackData: any = null;
  if (state === "hidden") {
    // 页面不可见
  }
  if (state === "visible") {
    // 页面可见
  }

  // 如果业务需要 时间超过1小时 则算是新开页面 自行判断
});

曝光

... 未完待续

埋点上报

我们采集了埋点数据后, 就需要把采集的数据交给后端。
那么我们应该如何上报?
我们得考虑接口跨域、上报异常(正在进行上报时, 用户关闭了页面, 这样上报就会失败)、性能要求(不能应用应用使用)

基于以上要求, 提供了三种方式供用户自行选择

  • sendBeacon
    浏览器引入的 sendBeacon 方法,发出的是异步请求,但是请求是作为浏览器任务执行的,
    与当前页面是脱钩的。因此该方法不会阻塞页面卸载流程和延迟后面页面的加载,用户体验较好。
    缺点: 浏览器存在支持问题

  • img

    避免跨域
    1x1 像素 img 对网页内容的影响几乎没有影响
    图片请求不占用 Ajax 请求限额
    不会阻塞页面加载,影响用户的体验
    相比 XMLHttpRequest 对象发送 GET 请求,性能上更好
    触发 GET 请求之后不需要获取和处理数据、服务器也不需要发送数据

  • post
    超出 get 最大请求数、或者用户手动进行上报方式配置则会采用 post

默认情况下优先级 sendBeacon > img > post

// 判断上传长度 2000 个字符
const urlIsLong = (url: string) => {
  let totalLength = 0,
    charCode = 0;
  for (var i = 0; i < url.length; i++) {
    charCode = url.charCodeAt(i);
    if (charCode < 0x007f) {
      totalLength++;
    } else if (0x0080 <= charCode && charCode <= 0x07ff) {
      totalLength += 2;
    } else if (0x0800 <= charCode && charCode <= 0xffff) {
      totalLength += 3;
    }
  }
  return totalLength < 2000 ? false : true;
};

if (navigator.sendBeacon) {
  sendBeacon(url, params);
} else if (method === "POST" || urlIsLong(str)) {
  xmlRequest(url, params);
} else {
  const img = new Image();
  img.src = `${url}?${str}`;
}

初始化

参数

参数 必填 默认值 类型
debug false bool 开启调试模式
config object {} 你的配置文件, 会在上报时传给后端
url '' string 请求地址
method img string 请求方式 GET、POST、SEND_BEACON
enableHeatMap false bool 开启坐标上传 position
enableVisibilitychange false bool 开启页面可见监听, 如开启此功能 registerVueRouterEvent 传参可能为 null

方法

方法名 说明 参数
setConfig 设置全局参数 Options
sendTracker 手动上报 {自定义}
initDirectives 初始化 vue2 指令 Vue
registerVueRouterEvent 初始化 VueRouter 监听 VueRouter, callback({to, from , secound,...}, callback)
registerErrorEvent 全局异常报错 vm: Vue 对象, errorCallback((errorMsg, pageInfo) => {}) 异常回调
import SimpleJsTracker from "simple-js-tracker";

const simpleJsTracker = new SimpleJsTracker({
  debug: true,
  url: "", // 服务地址
  enableHeatMap: true, // 开启热力图
  enableHashTracker: true,
  config: {
    ...
  }
});

// 更新传参
simpleJsTracker.setConfig(options);

// 自定义上传
simpleJsTracker.sendTracker(params);

// 初始化自定义vue2/3指令
simpleJsTracker.initDirectives(Vue);

// 初始化 VueRouter 监听
// 页面跳转监听, 上报的参数让用户自行提供 report
simpleJsTracker.registerVueRouterEvent(router, (res, report) => {
   const { to, from, secound } = res;
   // 页面进入
  if(to.meta.tracking) {
    const fromParams = {
      'event_type': 5,
      ...to.meta.tracking,
    }

    report(fromParams);
  }
  // 页面离开
  if(from.meta.tracking) {
    const fromParams = {
      'event_type': 6,
      ...from.meta.tracking,
    }
    report(fromParams);
  }
});

未完待续...

项目完整代码Github
SDK使用 npm

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.