From 4aafbc390c50c557d92d8f2c9c15afcf530ab600 Mon Sep 17 00:00:00 2001 From: DCloud_LXH <283700113@qq.com> Date: Tue, 1 Nov 2022 11:14:25 +0800 Subject: [PATCH] feat(h5): darkmode --- build/postcssSplitMediaPlugin.js | 30 +++ build/vue.config.js | 22 +- lib/h5/uni.config.js | 42 ++-- package.json | 1 + packages/uni-cli-shared/lib/index.js | 7 +- packages/uni-cli-shared/lib/theme.js | 4 +- packages/vue-cli-plugin-uni/lib/env.js | 13 +- .../packages/postcss/index.js | 29 ++- .../webpack-uni-pages-loader/lib/index-new.js | 11 +- .../lib/platforms/h5.js | 39 +++- src/core/helpers/constants.js | 5 +- src/core/service/api/device/theme.js | 6 +- src/platforms/h5/components/app/index.vue | 50 +++-- src/platforms/h5/components/app/observable.js | 16 +- src/platforms/h5/components/page/index.vue | 200 ++++++++++-------- src/platforms/h5/components/theme.js | 20 ++ .../h5/service/api/base/get-browser-info.js | 12 +- .../h5/service/api/device/get-system-info.js | 2 +- yarn.lock | 5 + 19 files changed, 373 insertions(+), 141 deletions(-) create mode 100644 build/postcssSplitMediaPlugin.js create mode 100644 src/platforms/h5/components/theme.js diff --git a/build/postcssSplitMediaPlugin.js b/build/postcssSplitMediaPlugin.js new file mode 100644 index 0000000000..1cf2f8c361 --- /dev/null +++ b/build/postcssSplitMediaPlugin.js @@ -0,0 +1,30 @@ +const mediaQuerys = [] + +module.exports = { + splitMediaPlugin: function (root, result) { + root.walkAtRules(rule => { + if (rule.params === '(perfers-color-scheme:dark)') { + root.removeChild(rule) + + mediaQuerys.push(rule) + } + }) + }, + generateMediaQuerys: function ({ outputDir, filename = 'index.dark.css' }) { + if (mediaQuerys.length) { + const fs = require('fs') + const path = require('path') + const postcss = require('postcss') + var uglifycss = require('uglifycss') + + const mediaRoot = postcss.root() + mediaRoot.append(mediaQuerys) + + fs.writeFileSync( + path.resolve(outputDir, filename), + uglifycss.processString(mediaRoot.toResult().css), + { encoding: 'utf-8', flag: 'w+' } + ) + } + } +} diff --git a/build/vue.config.js b/build/vue.config.js index e56156f22d..ccdb16510b 100644 --- a/build/vue.config.js +++ b/build/vue.config.js @@ -4,7 +4,11 @@ const resolve = dir => path.resolve(__dirname, '../', dir) const pkgPath = resolve('package.json') +const { splitMediaPlugin, generateMediaQuerys } = require('./postcssSplitMediaPlugin') +const webpack = require('webpack') + const webpackConfig = require('./webpack.config.js') +const postCssConfig = require('../postcss.config') let outputDir = resolve('./packages/uni-' + process.env.UNI_PLATFORM + '/dist') @@ -16,6 +20,8 @@ if (process.env.UNI_PLATFORM === 'app-plus' && process.env.UNI_VIEW === 'true') outputDir = resolve('./packages/uni-' + process.env.UNI_PLATFORM + '/dist') } +postCssConfig.plugins.push(splitMediaPlugin) + module.exports = { publicPath: '/', outputDir, @@ -41,8 +47,22 @@ module.exports = { configFile: pkgPath }) config.plugins.delete('hmr') // remove hot module reload + + config + .plugin('webpack-build-done') + .use(webpack.ProgressPlugin, [function (percentage, message, ...args) { + if (percentage === 1) { + console.log('webpack build done') + generateMediaQuerys({ + outputDir + }) + } + }]) }, css: { - extract: true + extract: true, + loaderOptions: { + postcss: postCssConfig + } } } diff --git a/lib/h5/uni.config.js b/lib/h5/uni.config.js index 0c1125fa09..d01bf81ccb 100644 --- a/lib/h5/uni.config.js +++ b/lib/h5/uni.config.js @@ -1,33 +1,32 @@ const fs = require('fs') const path = require('path') -function getTemplatePath(template) { +function getTemplatePath (template) { if (template) { const userTemplate = path.resolve(process.env.UNI_INPUT_DIR, template) - if (fs.existsSync(userTemplate)) - return userTemplate + if (fs.existsSync(userTemplate)) { return userTemplate } } return path.resolve(process.env.UNI_CLI_CONTEXT, 'public/index.html') } -function transform(content) { +function transform (content) { if (process.env.NODE_ENV === 'production') { return content + // shadow - `body::after{position:fixed;content:'';left:-1000px;top:-1000px;-webkit-animation:shadow-preload .1s;-webkit-animation-delay:3s;animation:shadow-preload .1s;animation-delay:3s}@-webkit-keyframes shadow-preload{0%{background-image:url(https://cdn.dcloud.net.cn/img/shadow-grey.png)}100%{background-image:url(https://cdn.dcloud.net.cn/img/shadow-grey.png)}}@keyframes shadow-preload{0%{background-image:url(https://cdn.dcloud.net.cn/img/shadow-grey.png)}100%{background-image:url(https://cdn.dcloud.net.cn/img/shadow-grey.png)}}` + 'body::after{position:fixed;content:\'\';left:-1000px;top:-1000px;-webkit-animation:shadow-preload .1s;-webkit-animation-delay:3s;animation:shadow-preload .1s;animation-delay:3s}@-webkit-keyframes shadow-preload{0%{background-image:url(https://cdn.dcloud.net.cn/img/shadow-grey.png)}100%{background-image:url(https://cdn.dcloud.net.cn/img/shadow-grey.png)}}@keyframes shadow-preload{0%{background-image:url(https://cdn.dcloud.net.cn/img/shadow-grey.png)}100%{background-image:url(https://cdn.dcloud.net.cn/img/shadow-grey.png)}}' } return content } -function getIndexCssPath(assetsDir, template) { +function getIndexCssPath (assetsDir, template, hashKey) { const CopyWebpackPluginVersion = Number(require('copy-webpack-plugin/package.json').version.split('.')[0]) - const VUE_APP_INDEX_CSS_HASH = process.env.VUE_APP_INDEX_CSS_HASH + const VUE_APP_INDEX_CSS_HASH = process.env[hashKey] if (VUE_APP_INDEX_CSS_HASH) { try { const templateContent = fs.readFileSync(getTemplatePath(template)) - if (/\bVUE_APP_INDEX_CSS_HASH\b/.test(templateContent)) { + if (new RegExp('\\b' + hashKey + '\\b').test(templateContent)) { return path.join(assetsDir, `[name].${VUE_APP_INDEX_CSS_HASH}${CopyWebpackPluginVersion > 5 ? '' : '.'}[ext]`) } - } catch (e) {} + } catch (e) { } } return assetsDir } @@ -40,10 +39,11 @@ module.exports = { filterTag: 'wxs', vue: '@dcloudio/vue-cli-plugin-uni/packages/h5-vue' }, - copyWebpackOptions(platformOptions, vueOptions) { - const copyOptions = [{ + copyWebpackOptions (platformOptions, vueOptions) { + const copyOptions = [ + { from: require.resolve('@dcloudio/uni-h5/dist/index.css'), - to: getIndexCssPath(vueOptions.assetsDir, platformOptions.template), + to: getIndexCssPath(vueOptions.assetsDir, platformOptions.template, 'VUE_APP_INDEX_CSS_HASH'), transform }, 'hybrid/html' @@ -51,6 +51,22 @@ module.exports = { global.uniModules.forEach(module => { copyOptions.push('uni_modules/' + module + '/hybrid/html') }) + + // darkmode + let darkPath = '' + try { + darkPath = require.resolve('@dcloudio/uni-h5/dist/index.dark.css') + } catch (error) { } + if (platformOptions.darkmode === true && darkPath) { + copyOptions.push({ + from: require.resolve('@dcloudio/uni-h5/dist/index.dark.css'), + to: getIndexCssPath(vueOptions.assetsDir, platformOptions.template, 'VUE_APP_INDEX_DARK_CSS_HASH'), + transform: (content) => { + + } + }) + } + return copyOptions } -} +} diff --git a/package.json b/package.json index e3eb922440..32dab8d490 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "shell-exec": "^1.0.2", "stricter-htmlparser2": "^3.9.6", "strip-json-comments": "^3.1.0", + "uglifycss": "^0.0.29", "vue": "^2.6.11", "vue-router": "^3.0.1", "vue-template-compiler": "^2.6.11", diff --git a/packages/uni-cli-shared/lib/index.js b/packages/uni-cli-shared/lib/index.js index 6d5b53f710..a2d06340e3 100644 --- a/packages/uni-cli-shared/lib/index.js +++ b/packages/uni-cli-shared/lib/index.js @@ -65,6 +65,8 @@ const { const uts = require('./uts') +const { parseTheme } = require('./theme') + module.exports = { uts, md5, @@ -115,5 +117,6 @@ module.exports = { getPlatformGlobal, getPlatformStat, getPlatformPush, - getPlatformUniCloud -} + getPlatformUniCloud, + parseTheme +} diff --git a/packages/uni-cli-shared/lib/theme.js b/packages/uni-cli-shared/lib/theme.js index efa1f21e2d..01c2e60ef7 100644 --- a/packages/uni-cli-shared/lib/theme.js +++ b/packages/uni-cli-shared/lib/theme.js @@ -47,8 +47,8 @@ module.exports = { console.error(e) } }, - parseTheme (json) { - const theme = global.uniPlugin.defaultTheme + parseTheme (json, _theme) { + const theme = themeConfig[_theme] || global.uniPlugin.defaultTheme if (!theme) { return json } diff --git a/packages/vue-cli-plugin-uni/lib/env.js b/packages/vue-cli-plugin-uni/lib/env.js index 369b0016aa..97e62dcc5a 100644 --- a/packages/vue-cli-plugin-uni/lib/env.js +++ b/packages/vue-cli-plugin-uni/lib/env.js @@ -35,6 +35,7 @@ process.env.UNI_APP_NAME = manifestJsonObj.name || '' process.env.UNI_PLATFORM = process.env.UNI_PLATFORM || 'h5' process.env.UNI_APP_VERSION_NAME = manifestJsonObj.versionName process.env.UNI_APP_VERSION_CODE = manifestJsonObj.versionCode +process.env.VUE_APP_DARK_MODE = (manifestJsonObj[process.env.UNI_PLATFORM] || {}).darkmode // 小程序 vue3 标记 if (process.env.UNI_PLATFORM.indexOf('mp-') === 0) { @@ -265,8 +266,16 @@ if (process.env.UNI_PLATFORM === 'h5') { process.env.UNI_OPT_PRELOAD = true } } - const buffer = fs.readFileSync(require.resolve('@dcloudio/uni-h5/dist/index.css')) - process.env.VUE_APP_INDEX_CSS_HASH = loaderUtils.getHashDigest(buffer, 'md5', 'hex', 8) + const indexCssBuffer = fs.readFileSync(require.resolve('@dcloudio/uni-h5/dist/index.css')) + process.env.VUE_APP_INDEX_CSS_HASH = loaderUtils.getHashDigest(indexCssBuffer, 'md5', 'hex', 8) + let indexDarkCssBuffer = '' + try { + indexDarkCssBuffer = fs.readFileSync(require.resolve('@dcloudio/uni-h5/dist/index.dark.css')) + process.env.VUE_APP_INDEX_DARK_CSS_HASH = loaderUtils.getHashDigest(indexDarkCssBuffer, 'md5', 'hex', 8) + } catch (error) { + process.env.VUE_APP_INDEX_DARK_CSS_HASH = '' + process.env.VUE_APP_DARK_MODE = false + } } if (process.env.UNI_PLATFORM === 'mp-qq') { // QQ小程序 强制自定义组件模式 diff --git a/packages/vue-cli-plugin-uni/packages/postcss/index.js b/packages/vue-cli-plugin-uni/packages/postcss/index.js index 5bbe58d530..9a57986ec9 100644 --- a/packages/vue-cli-plugin-uni/packages/postcss/index.js +++ b/packages/vue-cli-plugin-uni/packages/postcss/index.js @@ -6,6 +6,8 @@ if (process.env.UNI_USING_V3) { const valueParser = require('postcss-value-parser') const { + parseTheme, + getJson, getPlatformCssVars } = require('@dcloudio/uni-cli-shared') @@ -20,6 +22,7 @@ if (process.env.UNI_USING_V3) { } const cssVars = getPlatformCssVars() + const pageJson = getJson('pages.json', true) const transformSelector = (complexSelector, transformer) => { return selectorParser(transformer).processSync(complexSelector) @@ -170,6 +173,30 @@ if (process.env.UNI_USING_V3) { if (process.env.UNI_PLATFORM === 'h5') { // Transform CSS AST here + // darkmode + if ( + process.env.VUE_APP_DARK_MODE === 'true' && + root.source.input.file.indexOf('App.vue') !== -1 + ) { + const pageBGC = (pageJson.globalStyle || {}).backgroundColor || '' + if (pageBGC.indexOf('@') === 0) { + ['dark', 'light'].forEach(theme => { + const { backgroundColor } = parseTheme({ backgroundColor: pageBGC }, theme) + if (backgroundColor !== 'undefined') { + const mediaRoot = postcss.parse(` + @media (prefers-color-scheme: ${theme}) { + body, + uni-page-body { + background-color: ${backgroundColor}; + } + } + `) + root.nodes = [...mediaRoot.nodes, ...root.nodes] + } + }) + } + } + root.walkRules(rule => { let hasPage = false // Transform each rule here @@ -269,7 +296,7 @@ if (process.env.UNI_USING_V3) { const version = Number(require('postcss/package.json').version.split('.')[0]) - if (version < 8) { + if (version <= 8) { module.exports = postcss.plugin('postcss-uniapp-plugin', fn) } else { module.exports = function (opts) { diff --git a/packages/webpack-uni-pages-loader/lib/index-new.js b/packages/webpack-uni-pages-loader/lib/index-new.js index 58172d2fc2..99ed33b880 100644 --- a/packages/webpack-uni-pages-loader/lib/index-new.js +++ b/packages/webpack-uni-pages-loader/lib/index-new.js @@ -99,9 +99,16 @@ module.exports = function (content, map) { } } + const platformManifestJson = manifestJson[process.env.UNI_PLATFORM] || {} + const pagesJson = parseTheme(originalPagesJson) if (global.uniPlugin.defaultTheme) { - this.addDependency(path.resolve(process.env.UNI_INPUT_DIR, 'theme.json')) + this.addDependency( + path.resolve( + process.env.UNI_INPUT_DIR, + platformManifestJson.themeLocation || 'theme.json' + ) + ) } // 组件自动导入配置 @@ -120,7 +127,7 @@ module.exports = function (content, map) { if (process.env.UNI_PLATFORM === 'h5') { return this.callback( null, - require('./platforms/h5')(pagesJson, manifestJson, this), + require('./platforms/h5')(originalPagesJson, manifestJson, this), map ) } diff --git a/packages/webpack-uni-pages-loader/lib/platforms/h5.js b/packages/webpack-uni-pages-loader/lib/platforms/h5.js index 99de71e4c0..2d8a18bab5 100644 --- a/packages/webpack-uni-pages-loader/lib/platforms/h5.js +++ b/packages/webpack-uni-pages-loader/lib/platforms/h5.js @@ -14,6 +14,10 @@ const { addPageUsingComponents } = require('@dcloudio/uni-cli-shared/lib/pages') +const { + getTheme +} = require('@dcloudio/uni-cli-shared/lib/theme') + const compilerVersion = require('@dcloudio/webpack-uni-pages-loader/package.json')['uni-app'].compilerVersion const PLATFORMS = getPlatforms() @@ -119,14 +123,27 @@ const getPageComponents = function (inputDir, pagesJson) { always: 'float' } let titleNView = pageStyle.titleNView - titleNView = Object.assign({}, { - type: pageStyle.navigationStyle === 'custom' ? 'none' : 'default' - }, pageStyle.transparentTitle in titleNViewTypeList ? { - type: titleNViewTypeList[pageStyle.transparentTitle], - backgroundColor: 'rgba(0,0,0,0)' - } : null, typeof titleNView === 'object' ? titleNView : (typeof titleNView === 'boolean' ? { - type: titleNView ? 'default' : 'none' - } : null)) + titleNView = Object.assign( + {}, + { + type: pageStyle.navigationStyle === 'custom' ? 'none' : 'default' + }, + pageStyle.transparentTitle in titleNViewTypeList + ? { + type: titleNViewTypeList[pageStyle.transparentTitle], + backgroundColor: 'rgba(0,0,0,0)' + } + : null, + typeof titleNView === 'object' + ? titleNView + : ( + typeof titleNView === 'boolean' + ? { + type: titleNView ? 'default' : 'none' + } + : null + ) + ) if (titleNView.type === 'none' || titleNView.type === 'transparent') { windowTop = 0 } @@ -407,9 +424,9 @@ module.exports = function (pagesJson, manifestJson, loader) { const googleMapKey = sdkConfigs.maps && sdkConfigs.maps.google && sdkConfigs.maps.google.key const aMapKey = sdkConfigs.maps && sdkConfigs.maps.amap && sdkConfigs.maps.amap.key const aMapSecurityJsCode = - sdkConfigs.maps && sdkConfigs.maps.amap && sdkConfigs.maps.amap.securityJsCode + sdkConfigs.maps && sdkConfigs.maps.amap && sdkConfigs.maps.amap.securityJsCode const aMapServiceHost = - sdkConfigs.maps && sdkConfigs.maps.amap && sdkConfigs.maps.amap.serviceHost + sdkConfigs.maps && sdkConfigs.maps.amap && sdkConfigs.maps.amap.serviceHost let locale = manifestJson.locale locale = locale && locale.toUpperCase() !== 'AUTO' ? locale : '' @@ -422,6 +439,8 @@ global['____${h5.appid}____'] = true; delete global['____${h5.appid}____']; global.__uniConfig = ${JSON.stringify(pagesJson)}; global.__uniConfig.compilerVersion = '${compilerVersion}'; +global.__uniConfig.darkmode = ${JSON.stringify(h5.darkmode || false)}; +global.__uniConfig.themeConfig = ${JSON.stringify(getTheme())}; global.__uniConfig.uniPlatform = '${process.env.UNI_PLATFORM}'; global.__uniConfig.appId = '${process.env.UNI_APP_ID}'; global.__uniConfig.appName = '${process.env.UNI_APP_NAME}'; diff --git a/src/core/helpers/constants.js b/src/core/helpers/constants.js index 483b539c27..05c0bccbd3 100644 --- a/src/core/helpers/constants.js +++ b/src/core/helpers/constants.js @@ -1,5 +1,6 @@ export const NAVBAR_HEIGHT = 44 export const TABBAR_HEIGHT = 50 // 576:landscape phones,768:tablets,992:desktops,1200:large desktops -export const RESPONSIVE_MIN_WIDTH = 768 -export const UNI_STORAGE_LOCALE = 'UNI_LOCALE' +export const RESPONSIVE_MIN_WIDTH = 768 +export const UNI_STORAGE_LOCALE = 'UNI_LOCALE' +export const ON_THEME_CHANGE = 'onThemeChange' diff --git a/src/core/service/api/device/theme.js b/src/core/service/api/device/theme.js index 49e5ffeb15..a6543ad665 100644 --- a/src/core/service/api/device/theme.js +++ b/src/core/service/api/device/theme.js @@ -6,10 +6,14 @@ import { onMethod } from '../../platform' +import { + ON_THEME_CHANGE +} from 'uni-helpers/constants' + const callbacks = [] const oldCallbacks = [] -onMethod('onThemeChange', function (res) { +onMethod(ON_THEME_CHANGE, function (res) { callbacks.forEach(callbackId => { invoke(callbackId, res) }) diff --git a/src/platforms/h5/components/app/index.vue b/src/platforms/h5/components/app/index.vue index 9e64526bfb..3c61a85a1c 100644 --- a/src/platforms/h5/components/app/index.vue +++ b/src/platforms/h5/components/app/index.vue @@ -1,5 +1,5 @@