提交 2d288147 编写于 作者: fxy060608's avatar fxy060608

feat: CSS Modules and State-Driven Dynamic CSS (#3008)

上级 eed74010
......@@ -4,6 +4,7 @@ declare namespace NodeJS {
}
interface ProcessEnv {
NODE_ENV: 'production' | 'development' | 'test'
UNI_NODE_ENV: 'production' | 'development' | 'test'
UNI_PLATFORM: UniApp.PLATFORM
UNI_SUB_PLATFORM: 'quickapp-webview-huawei' | 'quickapp-webview-union'
UNI_INPUT_DIR: string
......
因为 它太大了无法显示 source diff 。你可以改为 查看blob
......@@ -26,7 +26,7 @@ import {
JOB_PRIORITY_RENDERJS,
queuePostActionJob,
} from '../scheduler'
import { decodeAttr } from '../utils'
import { decodeAttr, isCssVar } from '../utils'
import { patchVShow, VShowElement } from '../directives/vShow'
import { initRenderjs } from '../renderjs'
......@@ -128,6 +128,8 @@ export class UniComponent extends UniNode {
} else {
this.$props.style = newStyle
}
} else if (isCssVar(name)) {
this.$.style.setProperty(name, value as string)
} else {
value = decodeAttr(this.$ || $(this.pid).$, value)
if (!this.wxsPropsInvoke(name, value, true)) {
......@@ -136,7 +138,11 @@ export class UniComponent extends UniNode {
}
}
removeAttr(name: string) {
this.$props[name] = null
if (isCssVar(name)) {
this.$.style.removeProperty(name)
} else {
this.$props[name] = null
}
}
remove() {
......
......@@ -20,7 +20,7 @@ import {
JOB_PRIORITY_UPDATE,
queuePostActionJob,
} from '../scheduler'
import { decodeAttr } from '../utils'
import { decodeAttr, isCssVar } from '../utils'
import { patchVShow, VShowElement } from '../directives/vShow'
import { initRenderjs } from '../renderjs'
......@@ -131,6 +131,8 @@ export class UniElement<T extends object> extends UniNode {
value = decodeAttr(this.$, value)
if (this.$propNames.indexOf(name) !== -1) {
;(this.$props as any)[name] = value
} else if (isCssVar(name)) {
this.$.style.setProperty(name, value as string)
} else {
if (!this.wxsPropsInvoke(name, value)) {
this.$.setAttribute(name, value as string)
......@@ -140,6 +142,8 @@ export class UniElement<T extends object> extends UniNode {
removeAttribute(name: string) {
if (this.$propNames.indexOf(name) !== -1) {
delete (this.$props as any)[name]
} else if (isCssVar(name)) {
this.$.style.removeProperty(name)
} else {
this.$.removeAttribute(name)
}
......
......@@ -16,3 +16,7 @@ export function decodeAttr(el: UniCustomElement, value: unknown) {
}
return value
}
export function isCssVar(name: string) {
return name.indexOf('--') === 0
}
......@@ -11992,13 +11992,28 @@ export default function vueFactory(exports) {
if (vnode.shapeFlag & 1
/* ELEMENT */
&& vnode.el) {
var style = vnode.el.style;
setVarsOnNode(vnode.el, vars);
} else if (vnode.type === Fragment) {
vnode.children.forEach(c => setVarsOnVNode(c, vars));
} else if (vnode.type === Static) {
var {
el,
anchor
} = vnode;
while (el) {
setVarsOnNode(el, vars);
if (el === anchor) break;
el = el.nextSibling;
}
}
}
function setVarsOnNode(el, vars) {
if (el.nodeType === 1) {
for (var key in vars) {
style.setProperty("--".concat(key), vars[key]);
el.setAttribute("--".concat(key), vars[key]);
}
} else if (vnode.type === Fragment) {
vnode.children.forEach(c => setVarsOnVNode(c, vars));
}
}
......
......@@ -10134,13 +10134,28 @@ export default function vueFactory(exports) {
if (vnode.shapeFlag & 1
/* ELEMENT */
&& vnode.el) {
var style = vnode.el.style;
setVarsOnNode(vnode.el, vars);
} else if (vnode.type === Fragment) {
vnode.children.forEach(c => setVarsOnVNode(c, vars));
} else if (vnode.type === Static) {
var {
el,
anchor
} = vnode;
while (el) {
setVarsOnNode(el, vars);
if (el === anchor) break;
el = el.nextSibling;
}
}
}
function setVarsOnNode(el, vars) {
if (el.nodeType === 1) {
for (var key in vars) {
style.setProperty("--".concat(key), vars[key]);
el.setAttribute("--".concat(key), vars[key]);
}
} else if (vnode.type === Fragment) {
vnode.children.forEach(c => setVarsOnVNode(c, vars));
}
}
......
......@@ -9410,14 +9410,27 @@ function setVarsOnVNode(vnode, vars) {
vnode = vnode.component.subTree;
}
if (vnode.shapeFlag & 1 /* ELEMENT */ && vnode.el) {
const style = vnode.el.style;
for (const key in vars) {
style.setProperty(`--${key}`, vars[key]);
}
setVarsOnNode(vnode.el, vars);
}
else if (vnode.type === Fragment) {
vnode.children.forEach(c => setVarsOnVNode(c, vars));
}
else if (vnode.type === Static) {
let { el, anchor } = vnode;
while (el) {
setVarsOnNode(el, vars);
if (el === anchor)
break;
el = el.nextSibling;
}
}
}
function setVarsOnNode(el, vars) {
if (el.nodeType === 1) {
for (const key in vars) {
el.setAttribute(`--${key}`, vars[key]);
}
}
}
const TRANSITION = 'transition';
......
......@@ -43,6 +43,7 @@
"module-alias": "^2.2.2",
"postcss-import": "^14.0.2",
"postcss-load-config": "^3.1.0",
"postcss-modules": "^4.2.2",
"postcss-selector-parser": "^6.0.6",
"resolve": "^1.20.0",
"rollup-plugin-copy": "^3.4.0",
......
......@@ -3,6 +3,7 @@ import path from 'path'
import glob from 'fast-glob'
import chalk from 'chalk'
import postcssrc from 'postcss-load-config'
import { dataToEsm } from '@rollup/pluginutils'
import { PluginContext, RollupError, SourceMap } from 'rollup'
import {
// createDebugger,
......@@ -233,10 +234,14 @@ export function cssPostPlugin(
if (!cssLangRE.test(id) || commonjsProxyRE.test(id)) {
return
}
const modules = cssModulesCache.get(config)!.get(id)
const modulesCode =
modules && dataToEsm(modules, { namedExports: true, preferConst: true })
// build CSS handling ----------------------------------------------------
styles.set(id, css)
return {
code: '',
code: modulesCode || '',
map: { mappings: '' },
// avoid the css module from being tree-shaken so that we can retrieve
// it in renderChunk()
......
......@@ -157,4 +157,36 @@ describe('compiler: transform style', () => {
}`
)
})
test(`State-Driven Dynamic CSS`, () => {
assert(
`<view/>`,
`<view style="{{a}}"/>`,
`(_ctx, _cache) => {
return { a: _s(_ctx.__cssVars()) }
}`,
{
bindingCssVars: ['color'],
}
)
assert(
`<view :style="style"/>`,
`<view style="{{a + ';' + b}}"/>`,
`(_ctx, _cache) => {
return { a: _s(_ctx.style), b: _s(_ctx.__cssVars()) }
}`,
{
bindingCssVars: ['color'],
}
)
assert(
`<view :style="[style]"/>`,
`<view style="{{a + ';' + b}}"/>`,
`(_ctx, _cache) => {
return { a: _s(_ctx.style), b: _s(_ctx.__cssVars()) }
}`,
{
bindingCssVars: ['color'],
}
)
})
})
......@@ -15,6 +15,7 @@ import { transformElement } from './transforms/transformElement'
import { transformBind } from './transforms/vBind'
import { transformComponent } from './transforms/transformComponent'
import { transformSlot } from './transforms/vSlot'
import { transformRoot } from './transforms/transformRoot'
export type TransformPreset = [
NodeTransform[],
......@@ -29,7 +30,12 @@ export function getBaseTransformPreset({
skipTransformIdentifier: boolean
}): TransformPreset {
// order is important
const nodeTransforms = [transformIf, transformFor, transformSlot]
const nodeTransforms = [
transformRoot,
transformIf,
transformFor,
transformSlot,
]
if (!skipTransformIdentifier) {
nodeTransforms.push(transformIdentifier)
}
......
......@@ -65,6 +65,7 @@ export interface TransformOptions
isCustomElement?: (tag: string) => boolean | void
expressionPlugins?: ParserPlugin[]
skipTransformIdentifier?: boolean
bindingCssVars?: string[]
}
export interface CodegenRootScope {
......
......@@ -248,6 +248,7 @@ export function createTransformContext(
hashId = null,
scopeId = null,
filters = [],
bindingCssVars = [],
bindingMetadata = EMPTY_OBJ,
cacheHandlers = false,
prefixIdentifiers = false,
......@@ -320,6 +321,7 @@ export function createTransformContext(
hashId,
scopeId,
filters,
bindingCssVars,
bindingMetadata,
cacheHandlers,
prefixIdentifiers,
......
import {
ElementNode,
DirectiveNode,
findProp,
NodeTypes,
} from '@vue/compiler-core'
import { createBindDirectiveNode } from '@dcloudio/uni-cli-shared'
import { NodeTransform, TransformContext } from '../transform'
import { parseExpr } from '../ast'
import {
arrayExpression,
Expression,
identifier,
isArrayExpression,
} from '@babel/types'
import { genBabelExpr } from '../codegen'
export const transformRoot: NodeTransform = (node, context) => {
if (node.type !== NodeTypes.ROOT) {
return
}
if (context.bindingCssVars.length) {
node.children.forEach((child) => {
if (child.type !== NodeTypes.ELEMENT) {
return
}
addCssVars(child, context)
})
}
}
const CSS_VARS = '__cssVars()'
function addCssVars(node: ElementNode, context: TransformContext) {
const styleProp = findProp(node, 'style', true) as DirectiveNode
if (!styleProp) {
node.props.push(createBindDirectiveNode('style', CSS_VARS))
} else {
if (styleProp.exp?.type === NodeTypes.SIMPLE_EXPRESSION) {
let expr = parseExpr(styleProp.exp.content, context) as Expression
if (isArrayExpression(expr)) {
expr.elements.push(identifier(CSS_VARS))
} else {
expr = arrayExpression([expr as Expression, identifier(CSS_VARS)])
}
styleProp.exp.content = genBabelExpr(expr)
}
}
}
......@@ -18,6 +18,10 @@ import { createConfigResolved } from './configResolved'
import { emitFile, getFilterFiles, getTemplateFiles } from './template'
import { getNVueCssPaths } from '../plugins/pagesJson'
import {
SFCTemplateCompileOptions,
SFCTemplateCompileResults,
} from '@vue/compiler-sfc'
export interface UniMiniProgramPluginOptions {
vite: {
......@@ -78,6 +82,9 @@ export function uniMiniProgramPlugin(
let nvueCssEmitted = false
let resolvedConfig: ResolvedConfig
rewriteCompileTemplate()
return {
name: 'vite:uni-mp',
uni: uniOptions({
......@@ -148,3 +155,14 @@ export function uniMiniProgramPlugin(
},
}
}
function rewriteCompileTemplate() {
const compiler = require(resolveBuiltIn('@vue/compiler-sfc'))
const { compileTemplate } = compiler
compiler.compileTemplate = (
options: SFCTemplateCompileOptions
): SFCTemplateCompileResults => {
;(options.compilerOptions as any).bindingCssVars = options.ssrCssVars || []
return compileTemplate(options)
}
}
......@@ -4726,6 +4726,54 @@ function createVueApp(rootComponent, rootProps = null) {
return app;
}
function useCssModule(name = '$style') {
/* istanbul ignore else */
{
const instance = getCurrentInstance();
if (!instance) {
(process.env.NODE_ENV !== 'production') && warn$1(`useCssModule must be called inside setup()`);
return EMPTY_OBJ;
}
const modules = instance.type.__cssModules;
if (!modules) {
(process.env.NODE_ENV !== 'production') && warn$1(`Current instance does not have CSS modules injected.`);
return EMPTY_OBJ;
}
const mod = modules[name];
if (!mod) {
(process.env.NODE_ENV !== 'production') &&
warn$1(`Current instance does not have CSS module named "${name}".`);
return EMPTY_OBJ;
}
return mod;
}
}
/**
* Runtime helper for SFC's CSS variable injection feature.
* @private
*/
function useCssVars(getter) {
const instance = getCurrentInstance();
/* istanbul ignore next */
if (!instance) {
(process.env.NODE_ENV !== 'production') &&
warn$1(`useCssVars is called without current active component instance.`);
return;
}
initCssVarsRender(instance, getter);
}
function initCssVarsRender(instance, getter) {
instance.ctx.__cssVars = () => {
const vars = getter(instance.proxy);
const cssVars = {};
for (const key in vars) {
cssVars[`--${key}`] = vars[key];
}
return cssVars;
};
}
function withModifiers() { }
function createVNode$1() { }
......@@ -5133,4 +5181,4 @@ function createApp(rootComponent, rootProps = null) {
}
const createSSRApp = createApp;
export { EffectScope, Fragment, ReactiveEffect, Text, c, callWithAsyncErrorHandling, callWithErrorHandling, computed, createApp, createSSRApp, createVNode$1 as createVNode, createVueApp, customRef, d, defineComponent, defineEmits, defineExpose, defineProps, e, effect, effectScope, f, getCurrentInstance, getCurrentScope, h, inject, injectHook, isInSSRComponentSetup, isProxy, isReactive, isReadonly, isRef, logError, markRaw, mergeDefaults, mergeProps, n, nextTick, o, onActivated, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onDeactivated, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onScopeDispose, onUnmounted, onUpdated, patch, provide, proxyRefs, queuePostFlushCb, r, reactive, readonly, ref, resolveComponent, resolveDirective, resolveFilter, s, setCurrentRenderingInstance, setupDevtoolsPlugin, shallowReactive, shallowReadonly, shallowRef, stop, t, toHandlers, toRaw, toRef, toRefs, triggerRef, unref, useAttrs, useSSRContext, useSlots, version, w, warn$1 as warn, watch, watchEffect, watchPostEffect, watchSyncEffect, withAsyncContext, withCtx, withDefaults, withDirectives, withModifiers, withScopeId };
export { EffectScope, Fragment, ReactiveEffect, Text, c, callWithAsyncErrorHandling, callWithErrorHandling, computed, createApp, createSSRApp, createVNode$1 as createVNode, createVueApp, customRef, d, defineComponent, defineEmits, defineExpose, defineProps, e, effect, effectScope, f, getCurrentInstance, getCurrentScope, h, inject, injectHook, isInSSRComponentSetup, isProxy, isReactive, isReadonly, isRef, logError, markRaw, mergeDefaults, mergeProps, n, nextTick, o, onActivated, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onDeactivated, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onScopeDispose, onUnmounted, onUpdated, patch, provide, proxyRefs, queuePostFlushCb, r, reactive, readonly, ref, resolveComponent, resolveDirective, resolveFilter, s, setCurrentRenderingInstance, setupDevtoolsPlugin, shallowReactive, shallowReadonly, shallowRef, stop, t, toHandlers, toRaw, toRef, toRefs, triggerRef, unref, useAttrs, useCssModule, useCssVars, useSSRContext, useSlots, version, w, warn$1 as warn, watch, watchEffect, watchPostEffect, watchSyncEffect, withAsyncContext, withCtx, withDefaults, withDirectives, withModifiers, withScopeId };
......@@ -4726,7 +4726,55 @@ function createVueApp(rootComponent, rootProps = null) {
return app;
}
function useCssModule(name = '$style') {
/* istanbul ignore else */
{
const instance = getCurrentInstance();
if (!instance) {
(process.env.NODE_ENV !== 'production') && warn$1(`useCssModule must be called inside setup()`);
return EMPTY_OBJ;
}
const modules = instance.type.__cssModules;
if (!modules) {
(process.env.NODE_ENV !== 'production') && warn$1(`Current instance does not have CSS modules injected.`);
return EMPTY_OBJ;
}
const mod = modules[name];
if (!mod) {
(process.env.NODE_ENV !== 'production') &&
warn$1(`Current instance does not have CSS module named "${name}".`);
return EMPTY_OBJ;
}
return mod;
}
}
/**
* Runtime helper for SFC's CSS variable injection feature.
* @private
*/
function useCssVars(getter) {
const instance = getCurrentInstance();
/* istanbul ignore next */
if (!instance) {
(process.env.NODE_ENV !== 'production') &&
warn$1(`useCssVars is called without current active component instance.`);
return;
}
initCssVarsRender(instance, getter);
}
function initCssVarsRender(instance, getter) {
instance.ctx.__cssVars = () => {
const vars = getter(instance.proxy);
const cssVars = {};
for (const key in vars) {
cssVars[`--${key}`] = vars[key];
}
return cssVars;
};
}
function withModifiers() { }
function createVNode$1() { }
export { EffectScope, Fragment, ReactiveEffect, Text, callWithAsyncErrorHandling, callWithErrorHandling, computed, createVNode$1 as createVNode, createVueApp, customRef, defineComponent, defineEmits, defineExpose, defineProps, effect, effectScope, getCurrentInstance, getCurrentScope, inject, injectHook, isInSSRComponentSetup, isProxy, isReactive, isReadonly, isRef, logError, markRaw, mergeDefaults, mergeProps, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onDeactivated, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onScopeDispose, onUnmounted, onUpdated, patch, provide, proxyRefs, queuePostFlushCb, reactive, readonly, ref, resolveComponent, resolveDirective, resolveFilter, setCurrentRenderingInstance, shallowReactive, shallowReadonly, shallowRef, stop, toHandlers, toRaw, toRef, toRefs, triggerRef, unref, useAttrs, useSSRContext, useSlots, version, warn$1 as warn, watch, watchEffect, watchPostEffect, watchSyncEffect, withAsyncContext, withCtx, withDefaults, withDirectives, withModifiers, withScopeId };
export { EffectScope, Fragment, ReactiveEffect, Text, callWithAsyncErrorHandling, callWithErrorHandling, computed, createVNode$1 as createVNode, createVueApp, customRef, defineComponent, defineEmits, defineExpose, defineProps, effect, effectScope, getCurrentInstance, getCurrentScope, inject, injectHook, isInSSRComponentSetup, isProxy, isReactive, isReadonly, isRef, logError, markRaw, mergeDefaults, mergeProps, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onDeactivated, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onScopeDispose, onUnmounted, onUpdated, patch, provide, proxyRefs, queuePostFlushCb, reactive, readonly, ref, resolveComponent, resolveDirective, resolveFilter, setCurrentRenderingInstance, shallowReactive, shallowReadonly, shallowRef, stop, toHandlers, toRaw, toRef, toRefs, triggerRef, unref, useAttrs, useCssModule, useCssVars, useSSRContext, useSlots, version, warn$1 as warn, watch, watchEffect, watchPostEffect, watchSyncEffect, withAsyncContext, withCtx, withDefaults, withDirectives, withModifiers, withScopeId };
......@@ -56,6 +56,9 @@ export function initEnv(type: 'dev' | 'build', options: CliOptions) {
process.env.NODE_ENV = 'production'
}
}
// vite 会修改 NODE_ENV,存储在 UNI_NODE_ENV 中,稍后校正 NODE_ENV
process.env.UNI_NODE_ENV = process.env.VITE_USER_NODE_ENV =
process.env.NODE_ENV
process.env.UNI_CLI_CONTEXT = isInHBuilderX()
? path.resolve(process.env.UNI_HBUILDERX_PLUGINS!, 'uniapp-cli-vite')
......
......@@ -9,6 +9,10 @@ export function createDefine(
return extend(
{
__VUE_PROD_DEVTOOLS__: false,
__VUE_I18N_FULL_INSTALL__: true,
__VUE_I18N_LEGACY_API__: true,
__VUE_I18N_PROD_DEVTOOLS__: false,
__INTLIFY_PROD_DEVTOOLS__: false,
},
initDefine()
)
......
......@@ -115,6 +115,14 @@ export default function uniPlugin(
})
plugins.push(...uniPlugins)
// 执行 build 命令时,vite 强制了 NODE_ENV
// https://github.com/vitejs/vite/blob/main/packages/vite/src/node/build.ts#L405
// const config = await resolveConfig(inlineConfig, 'build', 'production')
// 在 @vitejs/plugin-vue 之前校正回来
if (process.env.UNI_NODE_ENV !== process.env.NODE_ENV) {
process.env.NODE_ENV = process.env.UNI_NODE_ENV
}
plugins.unshift(
vuePlugin(initPluginVueOptions(options, uniPlugins, uniPluginOptions))
)
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册