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

feat(ssr): support rpx

上级 b2105d80
......@@ -33,7 +33,7 @@ function proxy(target, track, trigger) {
const globalData = {};
const ssrServerRef = (value, key, shallow = false) => {
assertKey(key, shallow);
const ctx = vue.useSSRContext();
const ctx = vue.getCurrentInstance() && vue.useSSRContext();
let state;
if (ctx) {
const __uniSSR = ctx[UNI_SSR] || (ctx[UNI_SSR] = {});
......
......@@ -76,7 +76,7 @@ const globalData: Record<string, any> = {}
const ssrServerRef: SSRRef = (value, key, shallow = false) => {
assertKey(key, shallow)
const ctx = useSSRContext()
const ctx = getCurrentInstance() && useSSRContext()
let state: Record<string, any>
if (ctx) {
const __uniSSR = ctx[UNI_SSR] || (ctx[UNI_SSR] = {})
......
import fs from 'fs'
import path from 'path'
import { once } from '@dcloudio/uni-shared'
import { extend } from '@vue/shared'
import { once, defaultRpx2Unit } from '@dcloudio/uni-shared'
import { parseJson } from './json'
......@@ -12,3 +13,8 @@ export const parseManifestJson = (inputDir: string) => {
}
export const parseManifestJsonOnce = once(parseManifestJson)
export const parseRpx2UnitOnce = once((inputDir: string) => {
const { h5 } = parseManifestJsonOnce(inputDir)
return extend({}, defaultRpx2Unit, (h5 && h5.rpx) || {})
})
import { createRpx2Unit } from '../../src/dom/style'
const defaultRpx2Unit = {
unit: 'rem',
unitRatio: 10 / 320,
unitPrecision: 5,
}
const { unit, unitRatio, unitPrecision } = defaultRpx2Unit
describe('Rpx2Unit', () => {
test('rem', () => {
const rpx2unit = createRpx2Unit(unit, unitRatio, unitPrecision)
expect(rpx2unit('10')).toBe('10')
expect(rpx2unit('10rpx')).toBe('0.3125rem')
expect(rpx2unit('10upx')).toBe('0.3125rem')
expect(rpx2unit('color:red;height:10rpx;width:100px')).toBe(
'color:red;height:0.3125rem;width:100px'
)
})
})
......@@ -4,6 +4,27 @@ Object.defineProperty(exports, '__esModule', { value: true });
var shared = require('@vue/shared');
const unitRE = new RegExp(`"[^"]+"|'[^']+'|url\\([^)]+\\)|(\\d*\\.?\\d+)[r|u]px`, 'g');
function toFixed(number, precision) {
const multiplier = Math.pow(10, precision + 1);
const wholeNumber = Math.floor(number * multiplier);
return (Math.round(wholeNumber / 10) * 10) / multiplier;
}
const defaultRpx2Unit = {
unit: 'rem',
unitRatio: 10 / 320,
unitPrecision: 5,
};
function createRpx2Unit(unit, unitRatio, unitPrecision) {
return (val) => val.replace(unitRE, (m, $1) => {
if (!$1) {
return m;
}
const value = toFixed(parseFloat($1) * unitRatio, unitPrecision);
return value === 0 ? '0' : `${value}${unit}`;
});
}
function passive(passive) {
return { passive };
}
......@@ -324,9 +345,11 @@ exports.UNI_SSR_DATA = UNI_SSR_DATA;
exports.UNI_SSR_GLOBAL_DATA = UNI_SSR_GLOBAL_DATA;
exports.UNI_SSR_STORE = UNI_SSR_STORE;
exports.addFont = addFont;
exports.createRpx2Unit = createRpx2Unit;
exports.debounce = debounce;
exports.decode = decode;
exports.decodedQuery = decodedQuery;
exports.defaultRpx2Unit = defaultRpx2Unit;
exports.getEnvLocale = getEnvLocale;
exports.getLen = getLen;
exports.invokeArrayFns = invokeArrayFns;
......
......@@ -10,6 +10,8 @@ export declare const COMPONENT_PREFIX: string;
export declare const COMPONENT_SELECTOR_PREFIX = "uni-";
export declare function createRpx2Unit(unit: string, unitRatio: number, unitPrecision: number): (val: string) => string;
export declare function debounce(fn: Function, delay: number): {
(this: any): void;
cancel(): void;
......@@ -26,6 +28,12 @@ export declare function decode(text: string | number): string;
export declare function decodedQuery(query?: Record<string, any>): Record<string, string>;
export declare const defaultRpx2Unit: {
unit: string;
unitRatio: number;
unitPrecision: number;
};
export declare function getEnvLocale(): string;
export declare function getLen(str?: string): number;
......@@ -76,6 +84,8 @@ export declare function removeLeadingSlash(str: string): string;
export declare const RESPONSIVE_MIN_WIDTH = 768;
export declare type Rpx2UnitOptions = typeof defaultRpx2Unit;
export declare const sanitise: (val: unknown) => any;
declare function scrollTo_2(scrollTop: number | string, duration: number): void;
......
import { isString, isHTMLTag, isSVGTag, isPlainObject, isArray } from '@vue/shared';
const unitRE = new RegExp(`"[^"]+"|'[^']+'|url\\([^)]+\\)|(\\d*\\.?\\d+)[r|u]px`, 'g');
function toFixed(number, precision) {
const multiplier = Math.pow(10, precision + 1);
const wholeNumber = Math.floor(number * multiplier);
return (Math.round(wholeNumber / 10) * 10) / multiplier;
}
const defaultRpx2Unit = {
unit: 'rem',
unitRatio: 10 / 320,
unitPrecision: 5,
};
function createRpx2Unit(unit, unitRatio, unitPrecision) {
return (val) => val.replace(unitRE, (m, $1) => {
if (!$1) {
return m;
}
const value = toFixed(parseFloat($1) * unitRatio, unitPrecision);
return value === 0 ? '0' : `${value}${unit}`;
});
}
function passive(passive) {
return { passive };
}
......@@ -304,4 +325,4 @@ function getEnvLocale() {
return (lang && lang.replace(/[.:].*/, '')) || 'en';
}
export { BUILT_IN_TAGS, COMPONENT_NAME_PREFIX, COMPONENT_PREFIX, COMPONENT_SELECTOR_PREFIX, NAVBAR_HEIGHT, ON_REACH_BOTTOM_DISTANCE, PLUS_RE, PRIMARY_COLOR, RESPONSIVE_MIN_WIDTH, TABBAR_HEIGHT, TAGS, UNI_SSR, UNI_SSR_DATA, UNI_SSR_GLOBAL_DATA, UNI_SSR_STORE, addFont, debounce, decode, decodedQuery, getEnvLocale, getLen, invokeArrayFns, isBuiltInComponent, isCustomElement, isNativeTag, normalizeDataset, normalizeTarget, once, parseQuery, passive, plusReady, removeLeadingSlash, sanitise, scrollTo, stringifyQuery, updateElementStyle };
export { BUILT_IN_TAGS, COMPONENT_NAME_PREFIX, COMPONENT_PREFIX, COMPONENT_SELECTOR_PREFIX, NAVBAR_HEIGHT, ON_REACH_BOTTOM_DISTANCE, PLUS_RE, PRIMARY_COLOR, RESPONSIVE_MIN_WIDTH, TABBAR_HEIGHT, TAGS, UNI_SSR, UNI_SSR_DATA, UNI_SSR_GLOBAL_DATA, UNI_SSR_STORE, addFont, createRpx2Unit, debounce, decode, decodedQuery, defaultRpx2Unit, getEnvLocale, getLen, invokeArrayFns, isBuiltInComponent, isCustomElement, isNativeTag, normalizeDataset, normalizeTarget, once, parseQuery, passive, plusReady, removeLeadingSlash, sanitise, scrollTo, stringifyQuery, updateElementStyle };
import { FontFaceDescriptors } from 'css-font-loading-module'
import { isString } from '@vue/shared'
export * from './style'
export function passive(passive: boolean) {
return { passive }
}
......
const unitRE = new RegExp(
`"[^"]+"|'[^']+'|url\\([^)]+\\)|(\\d*\\.?\\d+)[r|u]px`,
'g'
)
function toFixed(number: number, precision: number) {
const multiplier = Math.pow(10, precision + 1)
const wholeNumber = Math.floor(number * multiplier)
return (Math.round(wholeNumber / 10) * 10) / multiplier
}
export const defaultRpx2Unit = {
unit: 'rem',
unitRatio: 10 / 320,
unitPrecision: 5,
}
export type Rpx2UnitOptions = typeof defaultRpx2Unit
export function createRpx2Unit(
unit: string,
unitRatio: number,
unitPrecision: number
) {
return (val: string) =>
val.replace(unitRE, (m, $1) => {
if (!$1) {
return m
}
const value = toFixed(parseFloat($1) * unitRatio, unitPrecision)
return value === 0 ? '0' : `${value}${unit}`
})
}
const context = (() => {
if (typeof globalThis !== 'undefined') {
return globalThis
} else if (typeof self !== 'undefined') {
return self
} else if (typeof window !== 'undefined') {
return window
} else {
return Function('return this')()
}
})()
// assign defines
const defines = __DEFINES__
Object.keys(defines).forEach((key) => {
const segments = key.split('.')
let target = context
let target = globalThis
for (let i = 0; i < segments.length; i++) {
const segment = segments[i]
if (i === segments.length - 1) {
......@@ -23,3 +11,14 @@ Object.keys(defines).forEach((key) => {
}
}
})
const { createRpx2Unit } = require('@dcloudio/uni-shared')
const rpx2unit = createRpx2Unit(__UNIT__, __UNIT_RATIO__, __UNIT_PRECISION__)
const shared = require('@vue/shared')
const oldStringifyStyle = shared.stringifyStyle
shared.stringifyStyle = (styles) => rpx2unit(oldStringifyStyle(styles))
const serverRender = require('@vue/server-renderer')
const oldSsrRenderStyle = serverRender.ssrRenderStyle
serverRender.ssrRenderStyle = (raw) =>
shared.isString(raw)
? rpx2unit(oldSsrRenderStyle(raw))
: oldSsrRenderStyle(raw)
......@@ -2,6 +2,8 @@ import path from 'path'
import fs from 'fs-extra'
import { UserConfig } from 'vite'
import autoprefixer from 'autoprefixer'
import { extend } from '@vue/shared'
import { parseRpx2UnitOnce } from '@dcloudio/uni-cli-shared'
import { VitePluginUniResolvedOptions } from '..'
import { uniapp } from '../utils'
......@@ -19,7 +21,12 @@ export function createCss(
return {
postcss: {
plugins: [
uniapp({ page: options.platform === 'h5' ? 'uni-page-body' : 'body' }),
uniapp(
extend(
{ page: options.platform === 'h5' ? 'uni-page-body' : 'body' },
parseRpx2UnitOnce(options.inputDir)
)
),
autoprefixer(),
],
},
......
......@@ -11,7 +11,7 @@ export function createDefine(
{ server }: UserConfig,
{ command }: ConfigEnv
): UserConfig['define'] {
const features = initFeatures({
return initFeatures({
inputDir,
command,
platform,
......@@ -19,8 +19,4 @@ export function createDefine(
manifestJson: parseManifestJsonOnce(inputDir),
ssr: !!(server && server.middlewareMode),
})
if (server && server.middlewareMode) {
Object.assign(globalThis, features)
}
return features
}
import { ResolvedConfig } from 'vite'
import { parserOptions } from '@vue/compiler-dom'
import { isNativeTag } from '@dcloudio/uni-shared'
import { rewriteSsrNativeTag, rewriteSsrRenderStyle } from '../utils'
// import alias from 'module-alias'
export function initConfig(config: ResolvedConfig) {
if (config.server.middlewareMode) {
// TODO compiler-ssr时,传入的 isNativeTag 会被 @vue/compiler-dom 的 isNativeTag 覆盖
// https://github.com/vuejs/vue-next/blob/master/packages/compiler-ssr/src/index.ts#L36
parserOptions.isNativeTag = isNativeTag
if (
(config.command === 'serve' && config.server.middlewareMode) ||
(config.command === 'build' && config.build.ssr)
) {
rewriteSsrNativeTag()
rewriteSsrRenderStyle(process.env.UNI_INPUT_DIR)
}
// let ssr = (config as any).ssr as SSROptions
// if (!ssr) {
......
......@@ -140,7 +140,7 @@ export function initPlugins(
addPlugin(
plugins,
uniSSRPlugin(extend({ exclude: [...COMMON_EXCLUDE] }, options)),
uniSSRPlugin(config, extend({ exclude: [...COMMON_EXCLUDE] }, options)),
'vite:vue'
)
......
......@@ -11,7 +11,12 @@ export function uniMainJsPlugin(
const mainJsPath = mainPath + '.js'
const mainTsPath = mainPath + '.ts'
const pagesJsonJsPath = slash(path.resolve(options.inputDir, 'pages.json.js'))
const isSSR = config.server.middlewareMode
const isSSR =
config.command === 'serve'
? !!config.server.middlewareMode
: config.command === 'build'
? !!(config.build.ssr || config.build.ssrManifest)
: false
return {
name: 'vite:uni-main-js',
transform(code, id, ssr) {
......
......@@ -33,13 +33,9 @@ export function uniPagesJsonPlugin(
if (id.endsWith(PAGES_JSON_JS)) {
return {
code:
(config.define!.__UNI_FEATURE_RPX__
? registerGlobalCode(ssr)
: '') +
(options.command === 'serve'
? registerDevServerGlobalCode(ssr)
: '') +
generatePagesJsonCode(ssr, code, config, options),
(options.command === 'serve' || (options.command === 'build' && ssr)
? registerGlobalCode(config, ssr)
: '') + generatePagesJsonCode(ssr, code, config, options),
map: { mappings: '' },
}
}
......@@ -100,15 +96,15 @@ function getGlobal(ssr?: boolean) {
return ssr ? 'global' : 'window'
}
function registerGlobalCode(ssr?: boolean) {
function registerGlobalCode(config: ResolvedConfig, ssr?: boolean) {
const name = getGlobal(ssr)
return `import {upx2px} from '@dcloudio/uni-h5'
${name}.rpx2px = upx2px
const rpx2pxCode = config.define!.__UNI_FEATURE_RPX__
? `import {upx2px} from '@dcloudio/uni-h5'
${name}.rpx2px = upx2px
`
}
function registerDevServerGlobalCode(ssr?: boolean) {
const name = getGlobal(ssr)
return `import {uni,getCurrentPages,getApp,UniServiceJSBridge,UniViewJSBridge} from '@dcloudio/uni-h5'
: ''
return `${rpx2pxCode}
import {uni,getCurrentPages,getApp,UniServiceJSBridge,UniViewJSBridge} from '@dcloudio/uni-h5'
${name}.getApp = getApp
${name}.getCurrentPages = getCurrentPages
${name}.uni = uni
......
import path from 'path'
import debug from 'debug'
import crypto from 'crypto'
import { Plugin } from 'vite'
import { Plugin, ResolvedConfig } from 'vite'
import { walk } from 'estree-walker'
import { CallExpression } from 'estree'
import { OutputChunk } from 'rollup'
import { createFilter } from '@rollup/pluginutils'
import { MagicString } from '@vue/compiler-sfc'
import { parseRpx2UnitOnce } from '@dcloudio/uni-cli-shared'
import { UniPluginFilterOptions } from '.'
import {
isIdentifier,
isCallExpression,
isMemberExpression,
generateSSRDefineCode,
generateSSREntryServerCode,
} from '../../utils'
......@@ -21,7 +26,10 @@ const KEYED_FUNC_RE = /(ssrRef|shallowSsrRef)/
const ENTRY_SERVER_JS = 'entry-server.js'
export function uniSSRPlugin(options: UniPluginFilterOptions): Plugin {
export function uniSSRPlugin(
config: ResolvedConfig,
options: UniPluginFilterOptions
): Plugin {
const filter = createFilter(options.include, options.exclude)
const entryServerJs = path.join(options.inputDir, ENTRY_SERVER_JS)
const entryServerJsCode = generateSSREntryServerCode()
......@@ -73,6 +81,18 @@ export function uniSSRPlugin(options: UniPluginFilterOptions): Plugin {
map: s.generateMap().toString(),
}
},
generateBundle(_options, bundle) {
const chunk = bundle['entry-server.js'] as OutputChunk
if (chunk) {
chunk.code =
generateSSRDefineCode(
config.define!,
parseRpx2UnitOnce(options.inputDir)
) +
'\n' +
chunk.code
}
},
}
}
......
import fs from 'fs'
import path from 'path'
import { ConfigEnv } from 'vite'
import { isArray } from '@vue/shared'
import { extend, isArray } from '@vue/shared'
interface ProjectFeatures {}
interface PagesFeatures {
......@@ -245,7 +245,7 @@ export function initFeatures(options: InitFeaturesOptions) {
initPagesFeature(options),
initProjectFeature(options)
)
return {
const features = {
__UNI_FEATURE_WX__: wx, // 是否启用小程序的组件实例 API,如:selectComponent 等(uni-core/src/service/plugin/appConfig)
__UNI_FEATURE_WXS__: wxs, // 是否启用 wxs 支持,如:getComponentDescriptor 等(uni-core/src/view/plugin/appConfig)
__UNI_FEATURE_RPX__: rpx, // 是否启用运行时 rpx 支持
......@@ -272,4 +272,9 @@ export function initFeatures(options: InitFeaturesOptions) {
__UNI_FEATURE_NAVIGATIONBAR_SEARCHINPUT__: navigationBarSearchInput, // 是否启用标题栏搜索框
__UNI_FEATURE_NAVIGATIONBAR_TRANSPARENT__: navigationBarTransparent, // 是否启用透明标题栏
}
// ssr nodejs features
if (options.ssr) {
extend(globalThis, features)
}
return features
}
......@@ -2,6 +2,8 @@ import { extend } from '@vue/shared'
import { rule, Rule, Declaration, Plugin } from 'postcss'
import selectorParser from 'postcss-selector-parser'
import {
createRpx2Unit,
defaultRpx2Unit,
isBuiltInComponent,
COMPONENT_SELECTOR_PREFIX,
} from '@dcloudio/uni-shared'
......@@ -13,12 +15,12 @@ interface UniAppCssProcessorOptions {
unitPrecision?: number // 单位精度,默认5
}
const defaultUniAppCssProcessorOptions = {
const defaultUniAppCssProcessorOptions = extend(
{
page: 'body',
unit: 'rem',
unitRatio: 10 / 320,
unitPrecision: 5,
}
},
defaultRpx2Unit
)
const BG_PROPS = [
'background',
......@@ -34,7 +36,7 @@ const BG_PROPS = [
function transform(
selector: selectorParser.Node,
opts: UniAppCssProcessorOptions,
page: string,
state: { bg: boolean }
) {
if (selector.type !== 'tag') {
......@@ -44,7 +46,6 @@ function transform(
if (isBuiltInComponent(value)) {
selector.value = COMPONENT_SELECTOR_PREFIX + value
} else if (value === 'page') {
const { page } = opts
if (!page) {
return
}
......@@ -67,52 +68,40 @@ function createBodyBackgroundRule(origRule: Rule) {
}
}
function walkRules(opts: UniAppCssProcessorOptions) {
function walkRules(page: string) {
return (rule: Rule) => {
const state = { bg: false }
rule.selector = selectorParser((selectors) =>
selectors.walk((selector) => transform(selector, opts, state))
selectors.walk((selector) => transform(selector, page, state))
).processSync(rule.selector)
state.bg && createBodyBackgroundRule(rule)
}
}
const unitRE = new RegExp(
`"[^"]+"|'[^']+'|url\\([^)]+\\)|(\\d*\\.?\\d+)[r|u]px`,
'g'
)
function toFixed(number: number, precision: number) {
const multiplier = Math.pow(10, precision + 1)
const wholeNumber = Math.floor(number * multiplier)
return (Math.round(wholeNumber / 10) * 10) / multiplier
}
function walkDecls(opts: Required<UniAppCssProcessorOptions>) {
function walkDecls(rpx2unit: ReturnType<typeof createRpx2Unit>) {
return (decl: Declaration) => {
const { value } = decl
if (value.indexOf('rpx') === -1 && value.indexOf('upx') === -1) {
return
}
decl.value = decl.value.replace(unitRE, (m, $1) => {
if (!$1) {
return m
}
const value = toFixed(parseFloat($1) * opts.unitRatio, opts.unitPrecision)
return value === 0 ? '0' : `${value}${opts.unit}`
})
decl.value = rpx2unit(decl.value)
}
}
export const uniapp = (opts?: UniAppCssProcessorOptions) => {
const options = extend({}, defaultUniAppCssProcessorOptions, opts || {})
const { page, unit, unitRatio, unitPrecision } = extend(
{},
defaultUniAppCssProcessorOptions,
opts || {}
)
const rpx2unit = createRpx2Unit(unit, unitRatio, unitPrecision)
return {
postcssPlugin: 'uni-app',
prepare() {
return {
OnceExit(root) {
root.walkDecls(walkDecls(options))
root.walkRules(walkRules(options))
root.walkDecls(walkDecls(rpx2unit))
root.walkRules(walkRules(page))
},
}
},
......
import path from 'path'
import fs from 'fs-extra'
import { isString, NormalizedStyle } from '@vue/shared'
import {
isNativeTag,
createRpx2Unit,
Rpx2UnitOptions,
} from '@dcloudio/uni-shared'
import { parseRpx2UnitOnce } from '@dcloudio/uni-cli-shared'
function serializeDefine(define: Record<string, any>): string {
let res = `{`
for (const key in define) {
const val = define[key]
res += `${JSON.stringify(key)}: ${
typeof val === 'string' ? `(${val})` : JSON.stringify(val)
typeof val === 'string' && !key.startsWith('process.env.') // process.env.* 必须序列化为字符串
? `(${val})`
: JSON.stringify(val)
}, `
}
return res + `}`
}
export function generateSSREnvCode(define: Record<string, any>): string {
export function generateSSRDefineCode(
define: Record<string, any>,
{ unit, unitRatio, unitPrecision }: Rpx2UnitOptions
): string {
return fs
.readFileSync(path.join(__dirname, '../../lib/ssr/env.js'), 'utf8')
.readFileSync(path.join(__dirname, '../../lib/ssr/define.js'), 'utf8')
.replace('__DEFINES__', serializeDefine(define))
.replace('__UNIT__', JSON.stringify(unit))
.replace('__UNIT_RATIO__', JSON.stringify(unitRatio))
.replace('__UNIT_PRECISION__', JSON.stringify(unitPrecision))
}
export function generateSSREntryServerCode() {
......@@ -24,3 +39,24 @@ export function generateSSREntryServerCode() {
'utf8'
)
}
export function rewriteSsrNativeTag() {
const { parserOptions } = require('@vue/compiler-dom')
// TODO compiler-ssr时,传入的 isNativeTag 会被 @vue/compiler-dom 的 isNativeTag 覆盖
// https://github.com/vuejs/vue-next/blob/master/packages/compiler-ssr/src/index.ts#L36
parserOptions.isNativeTag = isNativeTag
}
export function rewriteSsrRenderStyle(inputDir: string) {
const { unit, unitRatio, unitPrecision } = parseRpx2UnitOnce(inputDir)
const rpx2unit = createRpx2Unit(unit, unitRatio, unitPrecision)
const shared = require('@vue/shared')
const oldStringifyStyle = shared.stringifyStyle
shared.stringifyStyle = (styles: NormalizedStyle | undefined) =>
rpx2unit(oldStringifyStyle(styles))
const serverRender = require('@vue/server-renderer')
const oldSsrRenderStyle = serverRender.ssrRenderStyle
// 仅对字符串类型做转换,非字符串类型,通过 stringifyStyle 转换
serverRender.ssrRenderStyle = (raw: unknown) =>
isString(raw) ? rpx2unit(oldSsrRenderStyle(raw)) : oldSsrRenderStyle(raw)
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册