[转]Web Worker 使用教程

Web Worker 使用教程

一、概述

JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

Web Worker 有以下几个使用注意点。

(1) 同源限制

分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

(2) DOM 限制

Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用documentwindowparent这些对象。但是,Worker 线程可以navigator对象和location对象。

(3) 通信联系

Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。

(4) 脚本限制

Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。

(5) 文件限制

Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。

二、基本用法

2.1 主线程

主线程采用new命令,调用Worker()构造函数,新建一个 Worker 线程。

var worker = new Worker('work.js');

Worker()构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。由于 Worker 不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如404错误),Worker 就会默默地失败。

然后,主线程调用worker.postMessage()方法,向 Worker 发消息。

worker.postMessage('Hello World');
worker.postMessage({method: 'echo', args: ['Work']});

worker.postMessage()方法的参数,就是主线程传给 Worker 的数据。它可以是各种数据类型,包括二进制数据。

接着,主线程通过worker.onmessage指定监听函数,接收子线程发回来的消息。

worker.onmessage = function (event) {
  console.log('Received message ' + event.data);
  doSomething();
}

function doSomething() {
  // 执行任务
  worker.postMessage('Work done!');
}

上面代码中,事件对象的data属性可以获取 Worker 发来的数据。

Worker 完成任务以后,主线程就可以把它关掉。

worker.terminate();

2.2 Worker 线程

Worker 线程内部需要有一个监听函数,监听message事件。

self.addEventListener('message', function (e) {
  self.postMessage('You said: ' + e.data);
}, false);

上面代码中,self代表子线程自身,即子线程的全局对象。因此,等同于下面两种写法。

// 写法一
this.addEventListener('message', function (e) {
  this.postMessage('You said: ' + e.data);
}, false);

// 写法二
addEventListener('message', function (e) {
  postMessage('You said: ' + e.data);
}, false);

除了使用self.addEventListener()指定监听函数,也可以使用self.onmessage指定。监听函数的参数是一个事件对象,它的data属性包含主线程发来的数据。self.postMessage()方法用来向主线程发送消息。

根据主线程发来的数据,Worker 线程可以调用不同的方法,下面是一个例子。

self.addEventListener('message', function (e) {
  var data = e.data;
  switch (data.cmd) {
    case 'start':
      self.postMessage('WORKER STARTED: ' + data.msg);
      break;
    case 'stop':
      self.postMessage('WORKER STOPPED: ' + data.msg);
      self.close(); // Terminates the worker.
      break;
    default:
      self.postMessage('Unknown command: ' + data.msg);
  };
}, false);

上面代码中,self.close()用于在 Worker 内部关闭自身。

2.3 Worker 加载脚本

Worker 内部如果要加载其他脚本,有一个专门的方法importScripts()

importScripts('script1.js');

该方法可以同时加载多个脚本。

importScripts('script1.js', 'script2.js');

2.4 错误处理

主线程可以监听 Worker 是否发生错误。如果发生错误,Worker 会触发主线程的error事件。

worker.onerror(function (event) {
  console.log([
    'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
  ].join(''));
});

// 或者
worker.addEventListener('error', function (event) {
  // ...
});

Worker 内部也可以监听error事件。

2.5 关闭 Worker

使用完毕,为了节省系统资源,必须关闭 Worker。

// 主线程
worker.terminate();

// Worker 线程
self.close();

三、数据通信

前面说过,主线程与 Worker 之间的通信内容,可以是文本,也可以是对象。需要注意的是,这种通信是拷贝关系,即是传值而不是传址,Worker 对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。

主线程与 Worker 之间也可以交换二进制数据,比如 File、Blob、ArrayBuffer 等类型,也可以在线程之间发送。下面是一个例子。

// 主线程
var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i < uInt8Array.length; ++i) {
  uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);

// Worker 线程
self.onmessage = function (e) {
  var uInt8Array = e.data;
  postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
  postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};

但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。

如果要直接转移数据的控制权,就要使用下面的写法。

// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);

// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);

四、同页面的 Web Worker

通常情况下,Worker 载入的是一个单独的 JavaScript 脚本文件,但是也可以载入与主线程在同一个网页的代码。

<!DOCTYPE html>
  <body>
    <script id="worker" type="app/worker">
      addEventListener('message', function () {
        postMessage('some message');
      }, false);
    </script>
  </body>
</html>

上面是一段嵌入网页的脚本,注意必须指定<script>标签的type属性是一个浏览器不认识的值,上例是app/worker

然后,读取这一段嵌入页面的脚本,用 Worker 来处理。

var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);

worker.onmessage = function (e) {
  // e.data === 'some message'
};

上面代码中,先将嵌入网页的脚本代码,转成一个二进制对象,然后为这个二进制对象生成 URL,再让 Worker 加载这个 URL。这样就做到了,主线程和 Worker 的代码都在同一个网页上面。

五、实例:Worker 线程完成轮询

有时,浏览器需要轮询服务器状态,以便第一时间得知状态改变。这个工作可以放在 Worker 里面。

function createWorker(f) {
  var blob = new Blob(['(' + f.toString() +')()']);
  var url = window.URL.createObjectURL(blob);
  var worker = new Worker(url);
  return worker;
}

var pollingWorker = createWorker(function (e) {
  var cache;

  function compare(new, old) { ... };

  setInterval(function () {
    fetch('/my-api-endpoint').then(function (res) {
      var data = res.json();

      if (!compare(data, cache)) {
        cache = data;
        self.postMessage(data);
      }
    })
  }, 1000)
});

pollingWorker.onmessage = function () {
  // render data
}

pollingWorker.postMessage('init');

上面代码中,Worker 每秒钟轮询一次数据,然后跟缓存做比较。如果不一致,就说明服务端有了新的变化,因此就要通知主线程。

六、实例: Worker 新建 Worker

Worker 线程内部还能再新建 Worker 线程(目前只有 Firefox 浏览器支持)。下面的例子是将一个计算密集的任务,分配到10个 Worker。

主线程代码如下。

var worker = new Worker('worker.js');
worker.onmessage = function (event) {
  document.getElementById('result').textContent = event.data;
};

Worker 线程代码如下。

// worker.js

// settings
var num_workers = 10;
var items_per_worker = 1000000;

// start the workers
var result = 0;
var pending_workers = num_workers;
for (var i = 0; i < num_workers; i += 1) {
  var worker = new Worker('core.js');
  worker.postMessage(i * items_per_worker);
  worker.postMessage((i + 1) * items_per_worker);
  worker.onmessage = storeResult;
}

// handle the results
function storeResult(event) {
  result += event.data;
  pending_workers -= 1;
  if (pending_workers <= 0)
    postMessage(result); // finished!
}

上面代码中,Worker 线程内部新建了10个 Worker 线程,并且依次向这10个 Worker 发送消息,告知了计算的起点和终点。计算任务脚本的代码如下。

// core.js
var start;
onmessage = getStart;
function getStart(event) {
  start = event.data;
  onmessage = getEnd;
}

var end;
function getEnd(event) {
  end = event.data;
  onmessage = null;
  work();
}

function work() {
  var result = 0;
  for (var i = start; i < end; i += 1) {
    // perform some complex calculation here
    result += 1;
  }
  postMessage(result);
  close();
}

七、API

7.1 主线程

浏览器原生提供Worker()构造函数,用来供主线程生成 Worker 线程。

var myWorker = new Worker(jsUrl, options);

Worker()构造函数,可以接受两个参数。第一个参数是脚本的网址(必须遵守同源政策),该参数是必需的,且只能加载 JS 脚本,否则会报错。第二个参数是配置对象,该对象可选。它的一个作用就是指定 Worker 的名称,用来区分多个 Worker 线程。

// 主线程
var myWorker = new Worker('worker.js', { name : 'myWorker' });

// Worker 线程
self.name // myWorker

Worker()构造函数返回一个 Worker 线程对象,用来供主线程操作 Worker。Worker 线程对象的属性和方法如下。

  • Worker.onerror:指定 error 事件的监听函数。
  • Worker.onmessage:指定 message 事件的监听函数,发送过来的数据在Event.data属性中。
  • Worker.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
  • Worker.postMessage():向 Worker 线程发送消息。
  • Worker.terminate():立即终止 Worker 线程。

7.2 Worker 线程

Web Worker 有自己的全局对象,不是主线程的window,而是一个专门为 Worker 定制的全局对象。因此定义在window上面的对象和方法不是全部都可以使用。

Worker 线程有一些自己的全局属性和方法。

  • self.name: Worker 的名字。该属性只读,由构造函数指定。
  • self.onmessage:指定message事件的监听函数。
  • self.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
  • self.close():关闭 Worker 线程。
  • self.postMessage():向产生这个 Worker 线程发送消息。
  • self.importScripts():加载 JS 脚本。

(完)

Vue国际化

本文介绍Vue如何做国际化,包括vue-i18n, element ui 等国际化配置,搭配vuex, localStorage使用,切换语言无需刷新页面。

前言

项目基于@vue/cli3脚手架搭建,如果你没有自己的项目框架,建议使用这个脚手架(传送门)。有了项目框架后,先安装Vue国际化组件库vue-i18n,然后看一下项目结构图:


i18n文件夹的标准结构就是这样,可以照着在自己的项目中创建好对应的文件。i18n/index.js 是国际化入口配置文件。zh-CN/index.js, en-US/index.js 分别为中文和英文具体的文案文件,接下来挨个分析。

工具方法文件

文件位置:src/util.js

// 设置localStorage
export const setStorage = function(key, obj) {
    let json = JSON.stringify(obj) window.localStorage.setItem(key, json)
} 

// 获取localStorage
export const getStorage = function(key) {
    const str = window.localStorage.getItem(key) if (!str) {
        return null
    }
    return JSON.parse(str)
}

// 移除localStorage
export const removeStorage = function(key) {
    window.localStorage.removeItem(key)
}

// 获取浏览器默认语言
export const getBrowserLang = function() {
    let browserLang = navigator.language ? navigator.language: navigator.browserLanguage let defaultBrowserLang = ''
    if (browserLang.toLowerCase() === 'cn' || browserLang.toLowerCase() === 'zh' || browserLang.toLowerCase() === 'zh-cn') {
        defaultBrowserLang = 'zh-CN'
    } else {
        defaultBrowserLang = 'en-US'
    }
    return defaultBrowserLang
}

vue-i18n组件库入口文件配置

文件位置:src/i18n/index.js

使用localStorage存储用户选择的语言,方便用户下次进来不用再做切换操作。属性名为'lang',值为'zh-CN' 'en-US',分别代表中文、英文。

// 引入必要的库
import Vue from 'vue'
import VueI18n from 'vue-i18n'
// 引入element ui国际化文件
import elen from 'element-ui/lib/locale/lang/en'
import elcn from 'element-ui/lib/locale/lang/zh-CN'

// 引入工具函数
import { getStorage, removeStorage, getBrowserLang } from '@/util'

// 引入项目中需要用到的中英文文案配置js
import enLocale from './en-US'
import cnLocale from './zh-CN'

// 使用vue-i18n库
Vue.use(VueI18n)

// 获取当前语言(初始化时localStorage里没有存语言,默认为浏览器当前的语言)
const lang = getStorage('lang') || getBrowserLang()

// 组合element ui 和 项目自身的文案文件
const messages = {
    'en-US': {
        ...elen,
        ...enLocale
    },
    'zh-CN': {
        ...elcn,
        ...cnLocale
    }
}
// 创建vueI18n实例并输出,在main.js中调用
const i18n = new VueI18n({
    locale: lang,
    messages
})

export default i18n

文件位置:src/i18n/zh-CN/index.js

export default {  
    login: {    
        username: '用户名'  
    }
}

文件位置:src/i18n/en-US/index.js

export default {  
    login: {    
        username: 'username'  
    }
}

举个例子在登录页面组件中,用户名字段区分中英文。使用 {{ $t('login.username') }} 替换具体的文案即可。另外在js代码中通过 this.$t('login.username') 调用。

<el-form-item>
  {{ $t('login.username') }}<el-input v-model="form.username"></el-input>
</el-form-item>

main.js 引入 i18n 配置文件

import i18n from './i18n'

new Vue({
    router,
    i18n,
    store,
    render: h = >h(App)
}).$mount('#app')

语言切换组件

文件位置:src/views/component/Lang.vue

<template>
  <div>
    <el-dropdown @command="handleSetLang" trigger="click">
      <div class="lang-active">
        <div v-for="(lang, i) in langs" :key="`LangActive${i}`" v-show="lang.key === activeLang">{{ lang.value }}
          <i class="el-icon-arrow-down el-icon--right"></i></div>
      </div>
      <el-dropdown-menu slot="dropdown">
        <el-dropdown-item v-for="(lang, i) in langs" :key="`Lang${i}`" :command="lang.key">
          <span class="text">{{ lang.value }}</span></el-dropdown-item>
      </el-dropdown-menu>
    </el-dropdown>
  </div>
</template>

<script>
  import {
    mapGetters,
    mapMutations
  } from 'vuex';

  export default {
    name: 'Lang',
    components: {},
    data() {
      return {
        langs: [{
          key: 'zh-CN',
          value: '中文'
        },
        {
          key: 'en-US',
          value: 'EngLish'
        },
        ],
      }
    },
    computed: {
      ...mapGetters({
        activeLang: 'language'
      })
    },
    created() {},
    mounted() {},
    methods: {
      ...mapMutations(['setLanguage']),
      handleSetLang(lang) {
        // 设置i18n.locale 组件库会按照上面的配置使用对应的文案文件
        this.$i18n.locale = lang
        // 提交mutations 
        this.setLanguage(lang)
      }
    }
  }</script> 

使用element uidropdown 组件,大致效果如下,样式可自行调整


点击下拉菜单,调用 handleSetLang 方法,设置 this.i18n.locale 。如果 handleSetLang 方法里只写这句也可以实现语言切换效果,不过语言状态没有保存,刷新页面会重置到初始语言,这样肯定不是我们想要的。因此需要结合 localStoragevuex。如果对vuex不熟悉,可以先去官方文档学习一下。如果你的项目没有用到vuex,也可以只使用 localStorage 通过刷新页面来更新视图。代码如下:

handleSetLang(lang) {
    // 设置 i18n.locale
    this.$i18n.locale = lang 
    // 使用 localStorage 存储语言状态
    setStorage('lang', lang)
    // 刷新页面更新视图
    window.location.reload()
}

vuex相关配置

文件位置:src/store/index.js 建一个 language 模块,用来响应式更新语言。

import Vue from 'vue'
import Vuex from 'vuex'
import language from './modules/language'

Vue.use(Vuex) 

export default new Vuex.Store({
    modules: {
        language
    }
})

文件位置:src/store/language.js

import {
    getStorage,
    setStorage,
    getBrowserLang
}
from '@/util'

export default {
    state: {
    language: getStorage('lang') || getBrowserLang() // 项目初始化时,默认为浏览器的语言
    },
    getters: {
        language: state = > state.language
    },
    mutations: {
    setLanguage: (state, language) = > {
        state.language = language 
            setStorage('lang', language)
        },
    }
}

总结

使用 vue-i18n 组件库,在语言切换组件中设置 this.$i18n.locale ,vue-i18n 会根据配置文件使用对应的中英文文案文件。配合 localStorage 和 vuex 做到存储用户语言设置并动态更新,无需刷新页面。

vite vue-ts 配置 “@” 路径别名

序章、版本

"@types/node": "^16.9.1"
"vite": "^2.5.4"
"@vitejs/plugin-vue": "^1.6.1"
"@vue/compiler-sfc": "^3.2.6"
"vue-tsc": "^0.2.2"
"typescript": "^4.3.2"

只要实现了,没有报错,版本不一样也无所谓

一、安装依赖

npm i @types/node -D

二、修改 vite.config.js

import { defineConfig } from 'vite'
import { resolve } from 'path'

export default defineConfig {
    // ...
    resolve: {
        alias: {
            "@": resolve(__dirname, 'src'), // 路径别名
        },
        extensions: ['.js', '.json', '.ts'] // 使用路径别名时想要省略的后缀名,可以自己 增减
    }
    // ...
}

vite 官方文档中 不建议忽略 .vue 后缀的文,所以在 import 引入文件的时候需要加 .vue
https://cn.vitejs.dev/config/#resolve-extensions

import HelloWorld from '@/components/HelloWorld.vue'

三、修改tsconfig.json

{
    "compilerOptions" : {
        // ...
        "baseUrl": ".", // 用于设置解析非相对模块名称的基本目录,相对模块不会受到baseUrl的影响
        "paths": { // 用于设置模块名到基于baseUrl的路径映射
            "@/*": ["src/*"]
        }
        // ...
    }
}

vue项目打包增加版本信息

目标

在Vue项目打包后的 dist/index.html 文件中写入本地打包的git信息。方便测试确定线上当前的版本信息。

实施步骤

提取git相关信息

在构建时需要获取git相关的信息,这些信息都需要使用git命令来获取。在node中,要执行一段命令行脚本需要使用child_process模块

在项目build目录下新建 gitInfo.js 文件,文件内容如下:

const child_process = require('child_process')

const formatDate = function(date) {
    function pad(value) {
        return (value < 10 ? '0':'') + value
    }

    let year = date.getFullYear();
    let month = pad(date.getMonth() + 1);
    let day = pad(date.getDate());
    let hour = pad(date.getHours());
    let minutes = pad(date.getMinutes());
    let seconds = pad(date.getSeconds());

    return year + "-" + month + "-" + day + " " + hour + ":" + minutes + ":" + seconds
}

// git 最后一次提交的 Head
const commit = child_process.execSync('git show -s --format=%H').toString().trim()
const commitUserName = child_process.execSync('git show -s --format=%cn').toString().trim()
const commitUserMail = child_process.execSync('git show -s --format=%ce').toString().trim()
const commitDate = formatDate(new Date(child_process.execSync(`git show -s --format=%cd`).toString()))
const buildDate = formatDate(new Date())
const branch = child_process.execSync('git rev-parse --abbrev-ref HEAD').toString().replace(/\s+/, '')

module.exports = {commit, commitUserName, commitUserMail, commitDate, buildDate, branch}

配置 vue.config.js

在 vue.config.js 文件中引入 gitInfo.js 文件。

const gitInfo = require('./gitInfo.js')

chainWebpack: (config) => {
    // ...
    config.plugin('html').tap(args => {
        args[0].gitInfo = gitInfo
        return args
    })
},

index.html添加信息

接着在 index.html 文件中添加版本信息

<meta name="author"      content="<%= htmlWebpackPlugin.options.gitInfo.commitUserName %>">
<meta name="createdAt"   content="<%= htmlWebpackPlugin.options.gitInfo.commitDate %>">
<meta name="generatedAt" content="<%= htmlWebpackPlugin.options.gitInfo.buildDate %>">
<meta name="version"     content="<%= htmlWebpackPlugin.options.gitInfo.commit %>">
<meta name="branch"      content="<%= htmlWebpackPlugin.options.gitInfo.branch %>">