分类: 前端

  • async/await class constructor

    背景:期望在新建类时调后台接口对类进行配置,配置成功后才可以执行其他操作。

    尝试封装类的构造函数为async/await,报错

    Class constructor may not be an async method

    原因:async函数返回值为promise,而构造函数返回值为object,同一个函数不可能返回值同时为promiseobject

    解决:参考 jQuery's ready()方法

    1. 类实现

        class myClass {
            constructor () {
    
            }
    
            async ready (callback) {
                await yourHttpRequest();
                // 回调函数绑定作用域
                callback.bind(this)();
            }
        }

    2. 调用

        const myObj = new myClass();
        myObj.ready(function() {
            // 保证配置完才能执行其他操作
        });
    

    参考:https://stackoverflow.com/questions/43431550/async-await-class-constructor

  • Vue 3 TypeScript项目报错类型{}不存在属性

    搜了一下tsconfig.json文件需要配置"jsx": "preserve",但我的config文件原先就有这个配置。因为项目一开始是可以正常报错的,迁移到大仓后就有问题了。中间也考虑了是否是配置文件位置的问题,调整了位置也没有解决问题。

    最后看到tsconfig.json文件另一个报错“找不到node的类型定义文件”

    原因是我没有安装@types/node,装上后重新打开IDE”类型{}不存在属性“和“找不到node的类型定义文件”报错都没有了。

    // npm
    npm i @types/node --save-dev
    
    // pnpm
    pnpm add @types/node

  • 微信小程序保存图片到本地相册

    2022/10/29 更新

    可以利用canvas来绕过下载图片到本地受微信下载白名单的限制

    方案:

    1. canvas 绘制图片
    2. wx.canvasToTempFilePath拿临时路径
    3. wx.saveImageToPhotosAlbum 保存图片

    代码片段:https://developers.weixin.qq.com/s/iKZ2ymm87uDj

    原文章

    小程序使用wx.saveImageToPhotosAlbum保存图片到本地相册,该方法不能直接保存网络路径地址

    如果页面渲染的图片链接是网络地址,可以通过wx.downloadFile获取文件的本地路径 / 临时文件路径后,再调用保存图片方法

    除了保存图片到本地,如果还需要用到图片其他信息,可以使用wx.getImageInfo,该方法是对wx.downloadFile方法的封装,也会返回图片的本地路径,两个方法都需要先在管理后台配置下载白名单

  • Element Plus自定义命名空间

    使用Vue 3 + Element Plus开发新项目时,因为要引入的公共组件是Vue 2 + Element UI实现的。同一个项目中既有Element UI,又有Element Plus,会造成CSS冲突。

    官方给出的解决方案是给Element Plus修改命名空间,如:原来的都是el-开头,我们可以改成ep-开头。

    • 使用 ElConfigProvider 包装您的根组件
    <!-- App.vue -->
    <template>
      <el-config-provider namespace="ep">
        <!-- ... -->
      </el-config-provider>
    </template>
    
    • 在src目录下创建 styles/element/index.scss
    // styles/element/index.scss
    // we can add this to custom namespace, default is 'el'
    @forward 'element-plus/theme-chalk/src/mixins/config.scss' with (
      $namespace: 'ep'
    );
    // ...
    
    • vite.config.ts 中导入 styles/element/index.scss
    import { defineConfig } from 'vite'
    // https://vitejs.dev/config/
    export default defineConfig({
      // ...
      css: {
        preprocessorOptions: {
          scss: {
            additionalData: `@use "~/styles/element/index.scss" as *;`,
          },
        },
      },
      // ...
    })
    

    完全按照教程在全新的项目中试验会发现虽然类名都变成ep-开头了,但是element 源码中的类依然是el-

    原因:

    1. 官方教程中使用了~别名,如果你在vite / webpack中没有配置~别名,自定义命名空间就不会生效
    2. 如果是全部导入,需要在styles/element/index.scss中引入Element源码
    // we can add this to custom namespace, default is 'el'
    @forward "element-plus/theme-chalk/src/mixins/config.scss" with (
      $namespace: "ep"
    );
    
    @use "element-plus/theme-chalk/src/index.scss" as *;
    注意:如果是按需导入组件,需要使用ElementPlusResolver,具体配置参考官方模版
    
    import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
    
    Components({
          resolvers: [
            ElementPlusResolver({
              importStyle: 'sass',
            }),
          ]
    })
    
  • 修改 WordPress Twenty Fifteen 主题页脚以增加备案号

    感觉 Twenty Fifteen(2015)这个主题挺简洁,甚合我意。不过发现这个主题页脚没有提供可视化的设置,于是乎只能在主题编辑器上面看看页脚的源码(footer.php)怎么写的:

    <?php
    /**
     * The template for displaying the footer
     *
     * Contains the closing of the "site-content" div and all content after.
     *
     * @package WordPress
     * @subpackage Twenty_Fifteen
     * @since Twenty Fifteen 1.0
     */
    ?>
    
    	</div><!-- .site-content -->
    
    	<footer id="colophon" class="site-footer">
    		<div class="site-info">
    			<?php
    				/**
    				 * Fires before the Twenty Fifteen footer text for footer customization.
    				 *
    				 * @since Twenty Fifteen 1.0
    				 */
    				do_action( 'twentyfifteen_credits' );
    			?>
    			<?php
    			if ( function_exists( 'the_privacy_policy_link' ) ) {
    				the_privacy_policy_link( '', '<span role="separator" aria-hidden="true"></span>' );
    			}
    			?>
    			<a href="<?php echo esc_url( __( 'https://wordpress.org/', 'twentyfifteen' ) ); ?>" class="imprint">
    				<?php
    				/* translators: %s: WordPress */
    				printf( __( 'Proudly powered by %s', 'twentyfifteen' ), 'WordPress' );
    				?>
    			</a>
    		</div><!-- .site-info -->
    	</footer><!-- .site-footer -->
    
    </div><!-- .site -->
    
    <?php wp_footer(); ?>
    
    </body>
    </html>
    

    注意到第 23 行这里提供了一个钩子可以注入代码,如果在子主题里使用这个方法注入的话,就不需要修改原主题页脚的代码,这样的话代码量小,后期升级兼容性也更好。

    那么就新建一个子主题,在 functions.php 这里加载:

    add_action( 'twentyfifteen_credits', 'custom_footer_provider' );
    function custom_footer_provider() {
    	printf( '<a class="imprint" href="%s" target="_blank">%s</a>', 'https://beian.miit.gov.cn/', '粤ICP备XXX号-X' );
    }

    此时页脚已经可以显示备案号了,不过和原有的“自豪地采用WordPress”文字挤在一起,不太好看。注意到这两个元素其实是在一个 <div class="site-info"></div> 里的,那么只需要在 style.css 里面设置一下 flex 布局:

    .site-info {
    	display: flex;
    	justify-content: space-between;
    }

    此时两个文字已经贴靠两边显示了。

  • append vs appendChild

    append和appendChild都用于在DOM中添加新的元素,区别如下:

    1. append传参可以为DOMString和Node节点,appendChild传参只能为Node节点
    // 1. append传Node节点
    const parent = document.createElement('div');
    const child = document.createElement('p');
    parent.append(child);
    
    // 2. append传文本,被插入的 DOMString对象等价为 Text 节点
    const parent = document.createElement('div');
    parent.append('Appending Text');
    
    // 3. appendChild传Node节点
    const parent = document.createElement('div');
    const child = document.createElement('p');
    parent.appendChild(child);
    
    // 4. 报错:appendChild传文本
    const parent = document.createElement('div');
    parent.appendChild('Appending Text');
    appendChild不支持传字符串

    2. append没有返回值,appendChild返回被创建的Node节点

    const parent = document.createElement('div');
    const child = document.createElement('p');
    const appendValue = parent.append(child);
    console.log(appendValue) // undefined
    
    const appendChildValue = parent.appendChild(child);
    console.log(appendChildValue) // <p><p>
    

    3. append可以同时添加多个元素,appendChild同时只能添加一个

    const parent = document.createElement('div');
    const child = document.createElement('p');
    const childTwo = document.createElement('p');
    parent.append(child, childTwo, 'Hello world'); // Works fine
    
    parent.appendChild(child, childTwo, 'Hello world');
    // Works fine, but adds the first element and ignores the rest
    

    4. 浏览器兼容区别,appendChild在各浏览器中兼容性较好

    其他:DOMString是一个UTF-16字符串。由于JavaScript已经使用了这样的字符串,所以DOMString 直接映射到 一个String

  • Navigator Clipboard 复制不生效

    使用 navigator.clipboard.writeText 完成复制功能的实现时,在本地测试没有问题,部署后报错navigator.clipboard Cannot read property ‘writeText‘ of undefined

    原因:Navigator API 的安全策略禁用了非安全域的 navigator.clipboard 对象,API 仅支持通过 HTTPS 提供的页面。为帮助防止滥用,仅当页面是活动选项卡时才允许访问剪贴板。活动选项卡中的页面无需请求许可即可写入剪贴板,但从剪贴板读取始终需要许可。

    https://w3c.github.io/clipboard-apis/#dom-navigator-clipboard

    解决:判断当前环境是否支持navigator clipboard API,不允许则使用 document.execCommand('copy')进行剪贴板交互。
    使用兼容方案原因:navigator clipboard API是异步API,而使用document.execCommand('copy')进行剪贴板访问是同步的,只能读写 DOM,效率低下且在各浏览器之间还可能存在不同,在支持navigator clipboard API的情况下应尽量避免使用document.execCommand('copy')

    传入DOM ID

    export function copyCurrentTarget(text, id = '') {
        if (navigator.clipboard && window.isSecureContext) {
            navigator.clipboard.writeText(text)
        } else {
            window.getSelection().removeAllRanges()
            const questionToCopy = document.querySelector('#' + id)
            const range = document.createRange()
            range.selectNode(questionToCopy)
            window.getSelection().addRange(range)
            try {
                const successful = document.execCommand('copy')
                if (successful) {
                    console.log('复制成功')
                } 
            } catch (error) {
                console.error(error)
            }
        }
    }
    

    视口外创建一个新的DOM,传入要复制的内容

    function copyToClipboard(textToCopy) {
        if (navigator.clipboard && window.isSecureContext) {
            return navigator.clipboard.writeText(textToCopy)
        } else {
            let textArea = document.createElement("textarea")
            textArea.value = textToCopy
            textArea.style.position = "fixed"
            textArea.style.left = "-999999px"
            textArea.style.top = "-999999px"
            document.body.appendChild(textArea)
            textArea.focus()
            textArea.select()
            return new Promise((res, rej) => {
                document.execCommand('copy') ? res() : rej()
                textArea.remove()
            })
        }
    }
    

    扩展:安全和权限

    复制和粘贴权限已添加到 Permissions API 中。当页面处于活动标签页时,会自动授予 clipboard-write 权限。 clipboard-read 权限必须手动请求。如果尚未授予权限,尝试读取或写入剪贴板数据的操作会自动提示用户授予权限。

    const queryOpts = { name: 'clipboard-read', allowWithoutGesture: false };
    const permissionStatus = await navigator.permissions.query(queryOpts);
    // 'granted', 'denied' or 'prompt':
    console.log(permissionStatus.state);
    
    permissionStatus.onchange = () => {
      console.log(permissionStatus.state);
    };

    因为 Chrome 仅在页面是活动选项卡时才允许剪贴板访问,某些示例如果直接粘贴到 DevTools 中将无法运行,因为 DevTools 本身就是活动选项卡。有一个技巧:使用 setTimeout() 延迟剪贴板访问,然后在调用函数之前快速点击页面内部以将其聚焦:

    setTimeout(async () => {
    const text = await navigator.clipboard.readText();
    console.log(text);
    }, 2000);

    要在 iframe 中使用 API,需要使用权限策略启用它

    <iframe
        src="index.html"
        allow="clipboard-read; clipboard-write"
    >
    </iframe>

    参考:https://stackoverflow.com/questions/51805395/navigator-clipboard-is-undefined、https://developer.chrome.com/blog/cut-and-copy-commands/、https://web.dev/async-clipboard/

  • 判断对象为空

    ❌ 错误做法:

    let obj = {}
    obj == {} // false

    ✔ 正确做法:

    const isObjEmpty = (obj) => !Reflect.ownKeys(obj).length && obj.constructor === Object;

    注意:

    • 数组也可以使用Reflect.ownKeys方法,所以需要判断类型
    • 为什么不用Object.keys()Object.keys只能返回对象中可枚举的属性,Reflect.ownKeys返回所有属性
    var obj = {
        a: 1,
        b: 2
    }
    Object.defineProperty(obj, 'method', {
        value: function () {
        alert("Non enumerable property")
    },
        enumerable: false
    })
    ​
    console.log(Object.keys(obj))
    // ["a", "b"]
    console.log(Reflect.ownKeys(obj))
    // ["a", "b", "method"]
  • 层叠上下文和z-index

    渲染过程和层叠顺序

    浏览器将HTML解析为DOM的同时还创建了另一个树形结构,渲染树(render tree)。渲染树代表了每个元素的视觉样式和位置,同时决定浏览器绘制元素的顺序。

    • 通常情况下(没有使用定位),元素在HTML里出现的顺序决定了绘制顺序。后绘制的元素会出现在先绘制的元素前面。
    • 定位元素时,这种行为会改变。浏览器会先绘制所有非定位的元素,然后绘制定位元素。默认情况下,所有的定位元素会出现在非定位元素前面。

    使用z-index控制层叠顺序

    z-index属性的值可以是任意整数(正负都行)。z表示的是笛卡儿x-y-z坐标系里的深度方向。拥有较高z-index的元素出现在拥有较低z-index的元素前面。拥有负数z-index的元素出现在静态元素后面。

    z-index的行为很好理解,但是使用它时要注意两个小陷阱。

    • z-index只在定位元素上生效,不能用它控制静态元素。
    • 给一个定位元素加上z-index可以创建层叠上下文。

    层叠上下文

    一个层叠上下文包含一个元素或者由浏览器一起绘制的一组元素。其中一个元素会作为层叠上下文的根,比如给一个定位元素加上z-index的时候,它就变成了一个新的层叠上下文的根。所有后代元素就是这个层叠上下文的一部分。

    所有层叠上下文内的元素会按照以下顺序,从后到前叠放:

    • 层叠上下文的根
    • z-index为负的定位元素(及其子元素)
    • 非定位元素
    • z-indexauto的定位元素(及其子元素)
    • z-index为正的定位元素(及其子元素)

    举个例子

    下面这个例子可以用来理解层叠上下文,nested在第一个盒子的层叠上下文中,就算设置了很高的z-index,也会被第二个盒子遮挡。因为第一个盒子形成的层叠上下文在第二个盒子后面。

    <body>
      <div class="box one positioned">
        one
        <div class="absolute">nested</div>
      </div>
      <div class="box two positioned">two
      </div>
    
    </body>
    body {
      margin: 40px;
    }
    
    .box {
      display: inline-block;
      width: 200px;
      line-height: 200px;
      text-align: center;
      border: 2px solid black;
      background-color: #ea5;
      margin-left: -60px;
      vertical-align: top;
    }
    
    .one { margin-left: 0; }
    .two { margin-top: 30px; }
    
    
    .positioned {        (以下5行)每个定位的盒子都创建了一个层叠上下文,z-index为1
      position: relative;               
      background-color: #5ae;           
      z-index: 1;                      
    }                                  
    
    .absolute {
      position: absolute;
      top: 1em;
      right: 1em;
      height: 2em;
      background-color: #fff;
      border: 2px dashed #888;              
      z-index: 100;       ←---- z-index只控制元素在它所处层叠上下文内的层叠顺序
      line-height: initial;
      padding: 1em;
    }

    表现结果

    扩展到实际应用中的场景是,在已经打开的弹窗中再打开一个弹窗,就算第一个弹窗中的某个元素设置了很高的z-index,也还是会被第二个(后绘制)的弹窗挡住。