feng 发布的文章

将web worker打包到整个js代码中完整方案

我们在web端用js计算一些较为复杂耗时的运算时,常常需要用到Web Worker技术.他能够在子线程中运算,最终将结果返回主线程,使得主线程不会卡死.特别是3d场景游戏中,Web Worker使用会不可或缺.
在公司项目里,我有负责的canvas引擎开发中,经常会碰到一些需要用worker多线程的复杂运算场景.然而因为是引擎库,我更希望的是不将worker单独编译成一个js文件引入.
我这边的处理是将worker的代码变成字符串,然后通过Blob动态引入,具体代码如下:

    const script = `...` // worker 代码
    const workerBlob = new Blob([script],{type:'text/javascript'})
    const url = URL.createObjectURL(workerBlob)
    const worker = new Worker(url)
    return worker

到这里我们就可以把worker的代码打包进其他js文件,而不是单独的worker文件了,然而这样做最大的问题是些代码很不方便,并且不好调试(worker的代码往往涉及复杂的数学公式和逻辑运算)
我这边的做法是本地开发时,使用单独的worker js文件开发,只有在打包的时候,先将代码预处理成字符串,然后再通过Blob动态引入,关键代码如下:

export function getWorker(fileName:string){
    if ( IS_DEV ) {
        // 调试
        const FileWorker = require(`worker-loader!../worker/${fileName}.worker.js`)
        const worker = new FileWorker.default()
        return worker as Worker
    } else {
       // 打包 
       const scriptWorker = require(`../worker-lib/${fileName}.worker.js`)
       const script = scriptWorker.default
       // ... 跟上面的Blob方式一致
    }
}

预处理的node程序参考如下:
generate-worker.js

const inputPath = './src/lib/worker'
const outputPath = './src/lib/worker-lib'
var fs = require('fs')
var path = require('path')
var filePath = path.resolve(__dirname,inputPath)
var copyFilePath = path.resolve(__dirname,outputPath)

fileDisplay(filePath,copyFilePath)

function fileDisplay(filePath,copyFilePath){
    fs.mkdirSync(copyFilePath)
    const fileList = fs.readdirSync(filePath)
    for (let i = 0;i< fileList.length;i++) {
        const fileName = fileList[i]
        const fileDir = path.join(filePath,fileName)
        const text = fs.readFileSync(fileDir,'utf-8')
        const scriptText = `const script = \`${text}\`; export default script`
        
        const writeFilePath = path.join(copyFilePath,fileName)
        fs.writeFileSync(writeFilePath,scriptText,{ encoding:'utf-8' })
    }
    console.log('generate worker over')
}

然后再package.json中添加generate命令,用来生成worker字符串:
"generate":"rimraf ./src/lib/worker && node generate-worker.js"

然后在打包的命令前面运行generate,例如:npm run generate && npm run build

现在解决了在worker里调试js代码的问题,但是你要非常注意的使用js代码,因为这块js代码是不会经过babel编译,而是直接输出,很有可能调试的时候好好的,但是打包出来有问题(使用了浏览器还不支持的语法)

我这边给出的解决方案是直接用ts写worker代码,然后通过tsc命令将ts文件编译成js文件后,再调用我们写的generate-worker.js将js代码转成字符串,最后调用打包命令,打包已经转成字符串的代码.

我自己的worker目录管理:
ts写的worker文件位置: src/lib/worker
tsc编译成js文件的目录: src/lib/worker-template-lib
generate生成worker字符串的目录:src/lib/worker-lib

tsc命令: "rimraf ./src/lib/worker-template-lib && tsc --outDir ./src/lib/worker-template-lib src/lib/worker/* "

worker.ts 代码修改: const FileWorker = require(worker-loader!../worker/${fileName}.worker.ts)

generate-worker.js里面inputPath修改: const inputPath = './src/lib/worker-template-lib'

到这一步应该差不多能完美解决调试和打包 worker的需求了

查看three.js的自带Material中完整GLSL代码

有的时候我们写three.js自定义shaderMaterial的时候希望看一下three自带材质的着色器代码,但是源码里面都是代码片段,有时候读起来不是那么方便,这个时候可以在引入的源码或者node_modules里通过修改代码输出查看:

1: 打开引用的three.js或者node_modules下的three/build/three.module.js文件

搜索'*FRAGMENT*'

源码位置:

...
    const vertexGlsl = versionString + prefixVertex + vertexShader;
    const fragmentGlsl = versionString + prefixFragment + fragmentShader;

    // console.log( '*VERTEX*', vertexGlsl );
    // console.log( '*FRAGMENT*', fragmentGlsl );

    const glVertexShader = WebGLShader( gl, 35633, vertexGlsl );
    const glFragmentShader = WebGLShader( gl, 35632, fragmentGlsl );
...

取消注释即可 (VERTEX:顶点着色器,FRAGMENT:片元着色器)

本地开发微信登录流程记录

经历了两次完整的网站接入微信登录开发,第2次开发从注册账号到最终上线全部参与完成,觉得有必要把我的开发流程详细记录下来.供后面查阅

微信开放平台注册:

微信开放平台链接:https://open.weixin.qq.com/ 注册登录后,在管理中心的网站应用中,点击创建网站应用.

创建完成后点击进入应用,在接口信息下申请开通微信登录.开发信息里的授权回调域填写真实线上的域名.
开通成功后,在开放平台上记录下来AppID和AppSecret,开发后台接口时需要用到.

后台接口开发

官方文档: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
后台需要对接的接口一般有两个,一个是获取access_token和open_id,另外一个是根据access_token和openid获取用户信息.我这边是用egg开发,贴一下关键代码,非常简单

  // 根据code获取access_token
  async getAccessToken(code) {
    const wx = this.config.wxConfig
    const url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${wx.appid}&secret=${wx.appSecret}&code=${code}&grant_type=authorization_code`

    const res = await axios.get(url)

    return res.data
  }

  // 根据access_token和open_id获取用户信息(昵称,头像)
  async getUserInfo(accessToken, openid) {
    const url = `https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}&openid=${openid}&lang=zh_CN`
    const res = await axios.get(url)

    return res.data
  }
小提示:后台开发过程中可以直接用手机扫码拿到重定向链接里的code,然后用postman直接http请求,扫码链接微信有提供: https://open.weixin.qq.com/connect/qrconnect?appid=[AppID]&redirect_uri=[重定向链接]&response_type=code&scope=snsapi_login&state=123456#wechat_redirect,标记中括号的地方需要替换成真实信息,其中重定向链接是微信用户扫码确认后浏览器会自动访问这里填的链接,注意域名要和开放平台里填写的一致.

后端代码通过postman调试通过后,就是前端代码开发了.前端代码调试比较麻烦一点,这里详细讲一下我的解决方案:

生成ssl证书

这个可以自行搜索使用openssl创建证书,这里贴个链接供参考https://developer.aliyun.com/article/617249
然后本地安装nginx(也可以用其他的web服务器,我对nginx的转发比较熟而已)

nginx下载及启动

nginx下载地址:http://nginx.org/en/download.html 其中单数为测试,双数为稳定,如1.21.x就是测试,不过本地开发调试无所谓都没问题,下载好后解压在文件夹路径下打开cmd 运行 start ./nginx.exe就启动了(linux下安装和启动平时用的更多,就不说了)

nginx配置

nginx安装路径下找到./conf/nginx.conf 打开编辑添加 server配置:(可以写在默认配置的server 上面)

        ...
    server {
        listen                443    ssl;
        server_name            [线上真实域名];
        ssl_certificate        ../cert/ssl.crt;   # 证书位置
        ssl_certificate_key    ../cert/ssl.key;   # 同上
    
        location / {
              proxy_pass        http://127.0.0.1:xxxx;  # 前端启动后的端口号记得改
        }
    }
        ...

编辑完成后保存,nginx -s reload 重启
(启动失败的话注意一下本地的80端口和443端口是否有其他服务,常见的是window自带的IIS启动了的原因.

本地host配置

我们还需要把域名解析转发到本地来,window下的目录在 C:WindowsSystem32driversetchosts
添加一行127.0.0.1 线上真实域名
我这边是用的utools的hosts切换插件,切换起来非常方便(注意个别window启动hosts插件白屏的原因是根本没有那个文件,自己在目录下新建一个就好了)

然后就是开发调试了,本地前端代码启动后,不要访问http://localhost:xxxx ,直接访问https://线上真实域名 (nginx配置反向代理后会代理到你启动的端口号)

注意这里浏览器会提示你网站有危险(你自己本地创建的证书浏览器当然不认了),window直接点仍然访问就可以了,mac下的chrome需要在页面下输入:thisisunsafe

前端代码开发

这个没啥说的,简单的做法就是直接跳转 https://open.weixin.qq.com/connect/qrconnect?appid=[AppID]&redirect_uri=[重定向链接]&response_type=code&scope=snsapi_login&state=123456#wechat_redirect
我用的是引用文件https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js然后通过js加载二维码.

 var obj = new WxLogin({
 self_redirect:true,
 id:"login_container", 
 appid: "", 
 scope: "", 
 redirect_uri: "",
  state: "",
 style: "",
 href: ""
 });

然后在跳转的redirect_uri里面拿到url的code和state(假设你做了state为了验证登录是不是你的网站里发起的)
整个微信登录的开发其实很简单,主要是本地开发环境搭建麻烦点,这里记录一下以便将来再开发时查阅.

基于uniapp的日期区间选择组件开发

公司钉钉小程序有需求要让用户选择一个日期区间
在市场上没有找到相对比较匹配日期区间选择组件,于是考虑自己封装
公司的钉钉小程序用的是uniapp开发的,所以理所当然的基于uniapp开始了组件封装.
大概花了1天多时间简单做了一下封装,对钉钉和微信两个小程序端做了适配.
之前还没有针对picker-view做过封装,基本上用的都是现成的组件.这次封装算是对picker-view做下大概的了解.
不过因为是基于公司业务封装,功能也略偏向业务.

github地址:https://github.com/aoobao/dateRangePicker

页面效果展示:https://aoobao.github.io/dateRangePicker/demo.mp4

egg.js 中helper方法下的shtml 会删除无域名的src链接处理解决方案

开发网页或多或少会碰到要保存富文本内容的情况.但凡碰到需要保存富文本,服务端就必须开启xss防护.否则是非常不安全的.

egg.js默认集成了XSS框架,附上框架链接:
https://jsxss.com/zh/index.html
使用方法是 const safehtml = this.ctx.helper.shtml(html)
返回的html就是过滤后的html内容,可以安全保存在数据库中了.
里面有各种对不同的tag,不同属性的配置,这里就不举例了,大部分情况下默认配置就行,个别情况要改的话,根据默认配置增减就行.这里附上源码中的默认配置,以便查阅

https://github.com/leizongmin/js-xss/blob/master/lib/default.js

这里要说的主要内容是在egg.js中调用shtml,如果src或者href中的内容是'/'开头的没有域名,他是直接剔除掉的,附上源码:

    if (isWhiteAttr && (name === 'href' || name === 'src')) {
        if (!value) {
          return;
        }

        value = String(value);

        if (value[0] === '/' || value[0] === '#') {
          return;
        }

        const hostname = utils.getFromUrl(value, 'hostname');
        if (!hostname) {
          return;
        }

        // If we don't have our hostname in the app.security.domainWhiteList,
        // Just check for `shtmlConfig.domainWhiteList` and `ctx.whiteList`.
        if (!isSafeDomain(hostname, domainWhiteList)) {
          // Check for `shtmlConfig.domainWhiteList` first (duplicated now)
          if (shtmlConfig.domainWhiteList && shtmlConfig.domainWhiteList.length !== 0) {
            app.deprecate('[egg-security] `config.helper.shtml.domainWhiteList` has been deprecate. Please use `config.security.domainWhiteList` instead.');
            shtmlConfig.domainWhiteList = shtmlConfig.domainWhiteList.map(domain => domain.toLowerCase());
            if (!isSafeDomain(hostname, shtmlConfig.domainWhiteList)) {
              return '';
            }
          } else {
            return '';
          }
        }
      }

源码可以看到 if(value[0] === '/' || value[0] === '#'){ return }

但是一些情况下,在开发环境中,我们就是不想加域名,而是通过代理去实现图片访问,这个时候图片下的src属性值就是'/'开头.

通过源码可以看到,他合并了egg.js中的config,然后在最后判断了config中是否有onTagAttr方法,用来重写他的onTagAttr方法

    ...
    const shtmlConfig = utils.merge(this.app.config.helper.shtml, securityOptions.shtml);
    ...
    // avoid overriding user configuration 'onTagAttr'
    if (shtmlConfig.onTagAttr) {
      const original = shtmlConfig.onTagAttr;
      shtmlConfig.onTagAttr = function() {
        const result = original.apply(this, arguments);
        if (result !== undefined) {
          return result;
        }
        return shtmlConfig[BUILD_IN_ON_TAG_ATTR].apply(this, arguments);

      };
    } else {
      shtmlConfig.onTagAttr = shtmlConfig[BUILD_IN_ON_TAG_ATTR];
    }

我这里在config.default.js中添加了helper.shtml.onTagAttr方法,然后在config.prod.js文件中把他覆盖掉了,这样开发环境就可以使用'/'开头了.附上config.default.js中的配置代码:

const config  = {
// ...
    helper: {
      shtml: {
        onTagAttr(tag, name, value, isWhiteAttr) {
          if (isWhiteAttr && (name === 'href' || name === 'src')) {
            if (!value) {
              return
            }

            value = String(value)

            // 测试环境通过/做代理,不要过滤掉.
            if (value[0] === '/') {
              return `${name}="${value}"`
            }
          }
        },
      },
    },
// ...
}