1.22.4 • Published 1 month ago

nw-dark-mode v1.22.4

Weekly downloads
-
License
ISC
Repository
-
Last release
1 month ago

nw-dark-mode

LOFTER端内深色模式SDK

原理

本SDK主要通过SVG滤镜的方式实现页面的反色,再对图片视频局部元素二次反色使其保持原样

注意

  • 因为浏览器对于htmlbody节点上附加CSS Filter的实现标准没有统一,在这2个节点上设置反色滤镜可能会出现兼容性的问题,具体解释可见Stack Overflow
  • 所以,使用本SDK要求页面必须有一个非htmlbody元素的容器元素,并且在实例化时传入这个容器元素的选择器。(该容器宽高最好占满整个body,并且该容器的高度需要和内部元素的高度一致)
  • 如果htmlbody高度为100%,容器元素高度也为100%,但是内容高度却大于一屏时,在使用深色模式SVG滤镜时会覆盖不全,超出容器高度的部分可能会无法反色。
  • 本SDK深色模式主要基于CSS方案,所以并不要求选择器对应的元素已经加载完毕,如果想要在深色模式下进入H5就展示深色效果,可以将本SDK放入<head>元素中,或者放在React App的useLayoutEffect中。

实现方案

相关的客户端内H5的深色模式实现方案文档见内部WIKI。

内置CSS类

  • nw-dark-mode 深色模式下为html根节点设置的CSS类(可以配合本SDK的custom参数,自行实现深色模式的CSS适配)
  • nw-light-mode 浅色模式下为html根节点设置的CSS类,及时非客户端环境或者没有提供深浅色模式信息的客户端内也会设置(即只要没有识别到深色模式就会设置本CSS类)
  • nw-dark-preserve 为指定元素覆盖反向SVG滤镜,使其不在深色模式下被反色,可用于某些深色模式下不想被反色的图片或者icon等
  • nw-dark-filter 为指定元素手动设置SVG滤镜,只在深色模式下才有效果
  • nw-safari 判断当前浏览器环境是否为iOS safari或者iOS webview,如果是则会在html根节点设置此CSS类
  • nw-custom-dark-mode 设置darkMode.custom = true;时,会在html根节点设置的CSS类
  • nw-filter-dark-mode 设置darkMode.custom = false;时,会在html根节点设置的CSS类

太长不看版

  1. 复制以下代码到htmlhead中,在head的第一个css文件或者标签之前
  2. 修改下面代码中的#app为当前页面的容器CSS选择器,注意避免设置为bodyhtml
<script>
  (function(){
    window.containerCssSelector = '#app';
    !function(e){"function"==typeof define&&define.amd?define(e):e()}((function(){"use strict";var e="http://www.w3.org/2000/svg",t="dark-mode-filter",n="dark-mode-reverse-filter",o="dark-mode-svg",r="dark-mode-style",i="nw-dark-filter",a="nw-safari",d="nw-light-mode",l="nw-dark-mode",s=["img","video",":not(object):not(body)>embed","object","svg image",'[style*="background:url"]','[style*="background-image:url"]','[style*="background: url"]','[style*="background-image: url"]',"[background]",".nw-dark-preserve"],c=/ AppMode\/(\w+) /,u="lofter_app_mode";function p(){return/lofter/i.test(window.navigator.userAgent)}var m=function(){function m(e){var t=this;if(!(e=e||{containerCssSelector:"body"}).containerCssSelector)throw new Error("必须提供有效的容器CSS选择器");this.custom=e.custom||!1,this.changeHandlers=[],e.onModeChange&&"function"==typeof e.onModeChange&&this.changeHandlers.push(e.onModeChange),this.containerCssSelector=e.containerCssSelector,this.bridgeUrl="https://lofter.lf127.net/1631013208210/bridge.lofter.umd.js",this.autoStart(),window.addEventListener("pageshow",(function(e){if(e.persisted){console.log("触发bfcache,验证App模式");var n=t.getLocalMode();t.mode!==n&&t.setMode(n)}}))}return m.prototype.autoStart=function(){var e=this;this.checkSafari();var t=window.navigator.userAgent.match(c),n=t?t[1]:"light";this.setMode(n),p()&&this.loadBridge().then((function(t){t&&(console.log("Jsbridge脚本加载成功"),(0,t.registerHandler)("njb_noticePageStatus",(function(t){console.log("获取App模式更新通知",null==t?void 0:t.status),e.setMode(null==t?void 0:t.status)})))}))},m.prototype.checkSafari=function(){navigator.vendor&&navigator.vendor.indexOf("Apple")>-1?document.documentElement.classList.add(a):document.documentElement.classList.remove(a)},m.prototype.setMode=function(e){"dark"===e?this.dark():this.light()},m.prototype.loadBridge=function(){var e=this;return new Promise((function(t){if(p()){var n=document.createElement("script");n.src=e.bridgeUrl,n.onload=function(){window.Bridge&&window.Bridge.lofter?t(window.Bridge.lofter):t(null)},n.onerror=function(){t(null)},document.head.appendChild(n)}else t(null)}))},m.prototype.callback=function(){if(this.changeHandlers.length)for(var e=0;e<this.changeHandlers.length;e++){(0,this.changeHandlers[e])(this.mode)}},m.prototype.dark=function(){var e;this.custom||(this.addSvgFilter("0.333 -0.667 -0.667 0.000 1.000 -0.667 0.333 -0.667 0.000 1.000 -0.667 -0.667 0.333 0.000 1.000 0.000 0.000 0.000 1.000 0.000","0.333 -0.667 -0.667 0.000 1.000 -0.667 0.333 -0.667 0.000 1.000 -0.667 -0.667 0.333 0.000 1.000 0.000 0.000 0.000 1.000 0.000"),this.addCssStyle("@media screen {\n    /* Leading rule */\n    "+(e=this.containerCssSelector)+", ."+i+" {\n      -webkit-filter: url(#"+t+") !important;\n      filter: url(#"+t+") !important;\n    }\n\n    /* Reverse rule */\n    "+e.split(",").reduce((function(e,t){return e+"\n"+s.map((function(e){return t+" "+e+"{-webkit-filter: url(#"+n+") !important;filter: url(#"+n+") !important;}"})).join("\n")}),"")+'\n\n    [style*="background:url"] *,\n    [style*="background-image:url"] *,\n    [style*="background: url"] *,\n    [style*="background-image: url"] *,\n    input,\n    [background] *{\n      background: rgb(234,234,234) !important;\n    }\n\n    /* Text contrast */\n    html {\n      text-shadow: 0 0 0 !important;\n    }\n\n    /* Full screen */\n    :-webkit-full-screen, :-webkit-full-screen * {\n      -webkit-filter: none !important;\n      filter: none !important;\n    }\n    :-moz-full-screen, :-moz-full-screen * {\n      -webkit-filter: none !important;\n      filter: none !important;\n    }\n    :fullscreen, :fullscreen * {\n      -webkit-filter: none !important;\n      filter: none !important;\n    }\n\n    /* Container background */\n    '+e+", ."+i+" {\n      background: rgb(234,234,234) !important;\n    }\n\n  }")),this.addDarkClass(),this.mode="dark",this.saveLocalMode(this.mode),console.log("%c深色模式已启用","color: darkseagreen;"),this.callback()},m.prototype.light=function(){this.removeSvgFilter(),this.removeCssStyle(),this.addLightClass(),this.mode="light",this.saveLocalMode(this.mode),console.log("%c浅色模式已开启","color: brightGreen;"),this.callback()},m.prototype.saveLocalMode=function(e){localStorage.setItem(u,e)},m.prototype.getLocalMode=function(){return localStorage.getItem(u)},m.prototype.setDark=function(){this.loadBridge().then((function(e){if(e){var t=e.support,n=e.callHandler;t("njb_setAppMode")&&n("njb_setAppMode",{mode:"dark"})}}))},m.prototype.setLight=function(){this.loadBridge().then((function(e){if(e){var t=e.support,n=e.callHandler;t("njb_setAppMode")&&n("njb_setAppMode",{mode:"light"})}}))},m.prototype.createSvgELement=function(){var t=document.createElementNS(e,"svg");return t.id=o,t.style.height="0",t.style.width="0",t},m.prototype.createStyleELement=function(){var e=document.createElement("style");return e.type="text/css",e.id=r,e},m.prototype.addSvgFilter=function(r,i){if(!document.querySelector("#"+o)){var a=this.createSvgELement(),d=function(t,n){var o=document.createElementNS(e,"filter");return o.id=t,o.style.colorInterpolationFilters="sRGB",o.setAttribute("x","0"),o.setAttribute("y","0"),o.setAttribute("width","99999"),o.setAttribute("height","99999"),o.appendChild(function(t){var n=document.createElementNS(e,"feColorMatrix");return n.setAttribute("type","matrix"),n.setAttribute("values",t),n}(n)),o};a.appendChild(d(t,r)),a.appendChild(d(n,i)),document.head.appendChild(a)}},m.prototype.addCssStyle=function(e){if(!document.querySelector("#"+r)){var t=this.createStyleELement();t.textContent=e,document.head.appendChild(t)}},m.prototype.removeSvgFilter=function(){var e=document.querySelector("#"+o);e&&e.parentNode.removeChild(e)},m.prototype.removeCssStyle=function(){var e=document.querySelector("#"+r);e&&e.parentNode.removeChild(e);var t=document.querySelector("#preset-dark-style");t&&t.parentNode.removeChild(t)},m.prototype.addDarkClass=function(){document.documentElement.classList.remove(d),document.documentElement.classList.add(l)},m.prototype.addLightClass=function(){document.documentElement.classList.remove(l),document.documentElement.classList.add(d)},m.prototype.addChangeHandler=function(e){this.changeHandlers.push(e),e&&"function"==typeof e&&e(this.mode)},m}();window.darkModeInstance=new m({containerCssSelector:window.containerCssSelector})}));
  })()
</script>
  1. 如果需要在自己的应用获取或者监听App深浅色模式的切换,可以在后续的业务代码中,增加以下代码:
window.darkModeInstance && window.darkModeInstance.addChangeHandler(function (mode) {
  console.log('现在的深浅色模式是', mode);
})
  1. 如果需要切换App的深浅色模式,使用以下代码:
window.darkModeInstance.setDark();

FAQ

  1. 为什么我的页面在深色模式下会有一瞬间的白色闪烁

    如果没有在head的最开始使用本SDK,请检查页面中的容器以及部分主体元素的背景色,最好不要提前设置主体元素的背景色,或者在nw-light-modeclass下设置主体元素的背景色,保证深色sdk识别完成后再完成背景色的设置

  2. 深色模式下我fixedabsolute定位的元素位置不对了(Android和Chrome浏览器)

    本深色SDK使用了CSS filter实现,而该属性是会影响到容器内部元素的fixedabsolute定位的。 fixed 解决方式:

    1. 将绝对定位的元素放到SDK初始化的容器之外,根据nw-light-modenw-dark-modeclass自行适配
    2. 将SDK初始化的容器选择器设置为和绝对定位元素平级的其他元素,因为是纯CSS方案,选择器支持设置为.head,.main,.footer等等,然后依然自行适配绝对定位的元素
  3. iOS端深色模式下,我使用了fixedoverflow:autotransform这些CSS的元素没有应用到深色模式的滤镜

    这是因为iOS对于这类CSS的元素会单独为其设置一个渲染层,脱离了使用深色模式滤镜的渲染层,导致滤镜失效,此时有3种解决方案;

    1. 绕过:避开上述所述的几个CSS属性,使用其他属性代替(如果可以的话)
    2. 纯CSS:本SDK会自动识别iOS端的Safari或者webview,然后在document上添加nw-safariclass,所以遇到上述情况,可以手动添加css适配。比如:
    .footer{
      position: fixed;
      bottom: 0;
      left: 0;
      width: 100%;
      height: 1rem;
      background: #fff;
      color: #333;
     .nw-safari.nw-dark-mode &{
       background: #151515;
       color: #ccc
     }
    }
    1. 配合JS: 安装nw-detect包,引入isSafari方法,判断浏览器环境,手动为此类元素再添加一次深色模式滤镜,nw-dark-filter。此CSS类只在深色模式下有效,浅色模式无效,所以可以直接设置;但是不宜为太多元素设置此类,渲染层太多会有一定的性能问题。
    import { isSafari } from 'nw-detect'
    function Footer() {
      return <div className={`footer ${isSafari() ? 'nw-dark-filter' : ''}`}>footer</div>
    }

自定义使用方法

  1. 本SDK需要先实例化后才能使用,同一页面只能实例化一次本SDK
  2. 使用本SDK的页面建议不设置整个页面的背景色,交由客户端容器作为背景色
  3. 如果页面设计上需要背景色,如果不希望出现深色模式下的背景色闪烁,可以将本SDK放入head中初始化
  4. 本SDK在浅色模式下会在会在HTML根元素上添加CSS Class:nw-light-mode,在开启深色模式时会会变成nw-dark-mode
  5. benSDK识别到当前浏览器环境为Safari后,hiu在根元素上添加CSS Class:nw-safari
  6. 接入的页面可以针对此CSS class做一些适配,包括页面背景色如果一定要设置白色,最好放在SDK初始化脚本后,或者设置在nw-light-modeclass下,避免页面在深色模式下的闪烁
  7. 还可以给元素设置CSS Class:nw-dark-preserve来使元素不受SVG滤镜影响,再结合custom:true配置,实现精细的深色模式自定义适配(参照福利市集)
  8. 可以通过读取实例的mode属性获取当前的客户端深浅色模式,值为light|dark
  9. 实例化时可以传递auto、custom、onModeChange3个参数,具体见下面API解析

安装

$ npm install nw-dark-mode --save

使用

可参考源码中的测试页

  • es6方式引用
import DarkMode from 'nw-dark-mode';

const darkMode = new NWDarkMode(); console.log(darkMode.mode) // true darkMode.setDark() // 切换app到深色模式,切换后,app会通知h5切换后的模式,h5再响应变化 darkMode.setLight() // 切换app到浅色模式 darkMode.dark() // h5自己切换到深色模式,不通知app darkMode.light() // h5自己切换到浅色模式,不通知app

- browser中直接使用(需要拷贝打包后的js)
```html
<h1>测试页</h1>
<button class="nw-dark-preserve" id="switch">开关</button>
<script src="./index.umd.js"></script>
<script>
  window.darkMode = new NWDarkMode()
  var mode = darkMode.mode;
  var switchBtn = document.getElementById('switch');
  switchBtn.addEventListener('click', function () {
    if (mode === 'dark') {
      darkMode.light();
    } else {
      darkMode.dark();
    }
    mode = darkMode.mode;
  })
</script>
  • 初始化后动态修改containerCssSelector和custom
import DarkMode from 'nw-dark-mode';

const darkMode = new NWDarkMode();
// ...其他代码

darkMode.custom = false;
darkMode.containerCssSelector = '.className';
/**
 * 此时需要手动调用下updateView用以通知视图的更新
 */
darkMode.updateView();

发布

  1. 在当前目录下运行npm run build打包代码
  2. 在当前目录下运行npm run doc生成最新的文档
  3. 提交Git信息
  4. 在根目录运行npm run onlyPublish,发布组件更新

NWDarkMode

Kind: global class

new NWDarkMode(options)

ParamDescription
options实例化参数
options.containerCssSelector页面的容器元素CSS选择器,不建议设置为html或body
options.auto是否自动跟随客户端模式切换,默认为true
options.custom是否自行实现深色模式效果(html根节点class切换不受此参数影响),默认为false
options.onModeChange客户端模式切换回调函数,需参数auto为true时才能响应,默认为null

nwDarkMode.addChangeHandler()

Kind: instance method of NWDarkMode

nwDarkMode.dark()

Kind: instance method of NWDarkMode

nwDarkMode.light()

Kind: instance method of NWDarkMode

nwDarkMode.setDark() ⇒ boolean

Kind: instance method of NWDarkMode
Returns: boolean - 返回当前环境是否支持设置深浅色模式

nwDarkMode.setLight() ⇒ boolean

Kind: instance method of NWDarkMode
Returns: boolean - 返回当前环境是否支持设置深浅色模式

1.22.4

1 month ago

1.22.3

3 months ago

1.22.2

8 months ago

1.22.1

12 months ago

1.21.4

1 year ago

1.21.5

1 year ago

1.21.3

1 year ago

1.22.0

1 year ago

1.21.2

1 year ago

1.21.0

2 years ago

1.19.3

2 years ago

1.19.2

2 years ago

1.20.0

2 years ago

1.19.0

2 years ago

1.19.1

2 years ago

1.18.12

2 years ago

1.18.15

2 years ago

1.18.13

2 years ago

1.18.11

2 years ago

1.18.10

2 years ago

1.18.9

2 years ago

1.18.8

2 years ago

1.18.7

2 years ago

1.18.5

2 years ago

1.18.6

2 years ago

1.18.4

2 years ago

1.18.3

3 years ago

1.18.2

3 years ago

1.18.1

3 years ago

1.17.2

3 years ago

1.18.0

3 years ago

1.17.1

3 years ago

1.17.0

3 years ago

1.16.0

3 years ago

1.17.3

3 years ago

1.15.0

3 years ago

1.13.2

3 years ago

1.14.0

3 years ago

1.13.1

3 years ago

1.13.0

3 years ago

1.12.1

3 years ago

1.12.0

3 years ago

1.11.1

3 years ago

1.11.0

3 years ago

1.8.2

3 years ago

1.9.0

3 years ago

1.8.1

3 years ago

1.8.0

3 years ago

1.10.1

3 years ago

1.10.0

3 years ago

1.7.0

3 years ago

1.6.0

3 years ago

1.5.1

3 years ago

1.5.0

3 years ago

1.4.0

3 years ago

1.2.0

3 years ago

1.3.0

3 years ago

1.1.0

3 years ago

1.0.2

3 years ago