Browse Source

feat: demo add theme jdt

richard1015 3 years ago
parent
commit
ba77cfa106
4 changed files with 244 additions and 1 deletions
  1. 231 0
      src/sites/assets/util/helper.ts
  2. 4 0
      src/sites/config/env.ts
  3. 4 1
      src/sites/mobile/App.vue
  4. 5 0
      vite.config.ts

+ 231 - 0
src/sites/assets/util/helper.ts

@@ -0,0 +1,231 @@
+import config from '../../config/env';
+import { reactive, watch, onMounted, computed, onBeforeUnmount } from 'vue';
+import { nav } from '../../../config.json';
+
+type Obj = {
+  [k: string]: any;
+};
+
+type Store = {
+  variables: Obj[];
+  variablesMap: Obj;
+  rawStyles: string;
+  [k: string]: any;
+};
+
+const components = (nav as any[]).map(({ packages }) => (packages as any[]).map(({ name }) => name)).flat(1);
+
+const getRawFileText = async function (url: string) {
+  const response = await fetch(url);
+  const res = await response.text();
+  return res;
+};
+const getInputType = (value: string) => {
+  if (/^\d+$/.test(value)) {
+    return 'number';
+  }
+  if (/^#[A-Za-z0-9]+$/.test(value)) {
+    return 'hex';
+  }
+  if (/^(rgb|hsl)a?\((\s*\/?\s*[+-]?\d*(\.\d+)?%?,?\s*){3,5}\)/gim.test(value)) {
+    return 'rgb';
+  }
+
+  return 'input';
+};
+const loadScript = async (url: string) =>
+  new Promise((resolve, reject) => {
+    const script = document.createElement('script');
+    script.onload = resolve;
+    script.onerror = reject;
+    script.src = url;
+    document.head.appendChild(script);
+  });
+
+// 提取变量
+const extractVariables = (matched: string[], name: string, lowerCaseName: string) =>
+  matched.reduce((res, str) => {
+    const extract = str.replace(/\s+!default/, '').match(/(.*):(?:\s+)?([\s\S]*)(?:\s+)?;/);
+
+    if (extract) {
+      const key = extract[1];
+      const value = extract[2];
+      res.push({
+        name, // 组件名
+        lowerCaseName, // 组件名小写
+        key, // 变量名
+        rawValue: value, // 原始值
+        computedRawValue: '', // 计算后的原始值
+        value, // 编辑的值
+        // 编辑的类型
+        inputType: getInputType(value)
+      });
+    }
+    return res;
+  }, [] as Obj[]);
+
+// 提取样式代码,只保留有使用变量的行
+const extractStyle = (style: string) => {
+  if (!store.variables.length) {
+    return '';
+  }
+
+  // comment
+  style = style
+    .split('\n')
+    .filter((str) => !/^(\s+)?\/\//.test(str))
+    .join('\n');
+  // todo: parse mixin
+  style = style
+    .split('\n')
+    .filter((str) => !/^(\s+)?@include/.test(str))
+    .join('\n');
+
+  style = style.replace(/(?:({|;|\s|\n))[\w-]+:([^;{}]|;base64)+;(?!base64)/g, (matched) => {
+    const matchedKey = matched.match(/\$[\w-]+\b/g);
+    if (matchedKey && matchedKey.some((k) => store.variablesMap[k])) {
+      return matched;
+    }
+    return '';
+  });
+
+  // console.log(style);
+
+  return style;
+};
+
+const parseSassVariables = (text: string, components: string[]) => {
+  const matchedComponentVariables = components
+    .map((name) => {
+      const lowerCaseName = name.toLowerCase();
+      const reg = new RegExp(
+        `(?<!\\/\\/(\\s+)?)\\$(${name}|${lowerCaseName})\\b[\\w-]+:([^;{}]|;base64)+;(?!base64)`,
+        'g'
+      );
+      const matched = text.match(reg);
+      if (matched) {
+        return extractVariables(matched, name, lowerCaseName);
+      }
+    })
+    .filter(Boolean)
+    .flat(2);
+
+  const baseVariablesReg = new RegExp(
+    `\\$(?!(${matchedComponentVariables
+      .map((item) => (item && `${item.name}|${item.lowerCaseName}`) || '')
+      .join('|')})\\b)[\\w-]+:[^:]+;`,
+    'g'
+  );
+
+  const variables = matchedComponentVariables as Obj[];
+
+  const matchedBaseVariables = text.match(baseVariablesReg);
+
+  // 组件变量以外的都作为基础变量
+  if (matchedBaseVariables) {
+    variables.unshift(...extractVariables(matchedBaseVariables, 'Base', 'base'));
+  }
+
+  return variables;
+};
+
+let cachedStyles = '';
+const store: Store = reactive({
+  init: false,
+  variables: [],
+  variablesMap: {},
+  rawStyles: ''
+});
+
+const getSassVariables = async () => {
+  // 固定自定义主题的访问链接: https://nutui.jd.com/theme/?theme=自定义变量的文件地址#/
+  // e.g. https://nutui.jd.com/theme/?theme=xxx.com%2variables.scss#/
+  // vite issue https://github.com/vitejs/vite/issues/6894
+  const params = new URLSearchParams(window.parent.location.search);
+  const param = params.get('theme') as string;
+  const source = {
+    jdt: 'https://storage.360buyimg.com/nutui-static/source/variables-jdt.scss_source'
+  } as any;
+  const customUrl = param && source[param.replace('/', '')];
+  if (customUrl) {
+    const customVariablesText = await getRawFileText(customUrl);
+    const customVariables = parseSassVariables(customVariablesText, components);
+
+    const variablesMap = customVariables.reduce((map, item) => {
+      map[item.key] = 1;
+      return map;
+    }, {});
+    store.variables = customVariables;
+    store.variablesMap = variablesMap;
+  }
+};
+
+export const getRawSassStyle = async (): Promise<void> => {
+  const style = await getRawFileText(`${config.themeUrl}/styles/sass-styles.scss_source`);
+  store.rawStyles = style;
+};
+
+export const useThemeEditor = function () {
+  const cssText = computed(() => {
+    const variablesText = store.variables.map(({ key, value }) => `${key}:${value}`).join(';');
+    cachedStyles = cachedStyles || extractStyle(store.rawStyles);
+    return `${variablesText};${cachedStyles}`;
+  });
+
+  onMounted(async () => {
+    if (!store.init) {
+      await Promise.all([
+        getSassVariables(),
+        loadScript('https://cdnout.com/sass.js/sass.sync.min.js'),
+        getRawSassStyle()
+      ]);
+      store.init = true;
+    }
+  });
+
+  let timer: any = null;
+  onBeforeUnmount(() => {
+    clearTimeout(timer);
+  });
+  watch(
+    () => cssText.value,
+    (css: string) => {
+      clearTimeout(timer);
+      timer = setTimeout(() => {
+        const Sass = (window as any).Sass;
+        let beginTime = new Date().getTime();
+        console.log('sass编译开始', beginTime);
+        Sass &&
+          Sass.compile(css, async (res: Obj) => {
+            const iframe = window as any;
+            if (res.text && iframe) {
+              console.log('sass编译成功', new Date().getTime() - beginTime);
+              try {
+                if (!iframe.__styleEl) {
+                  const style = iframe.document.createElement('style');
+                  style.id = 'theme';
+                  iframe.__styleEl = style;
+                }
+                iframe.__styleEl.innerHTML = res.text;
+                iframe.document.head.appendChild(iframe.__styleEl);
+                console.info('insert success!');
+              } catch (error) {
+                console.error(error);
+              }
+            } else {
+              console.log('sass编译失败1s 重新加载', new Date().getTime() - beginTime);
+              setTimeout(() => {
+                window.location.reload();
+              }, 1000);
+              console.error(res);
+            }
+
+            if (res.status !== 0 && res.message) {
+              console.log(res.message);
+            }
+          });
+      }, 300);
+    },
+    { immediate: true }
+  );
+};

+ 4 - 0
src/sites/config/env.ts

@@ -1,5 +1,6 @@
 type EnvConfig = {
   baseUrl: string;
+  themeUrl: string;
   isPrd: boolean;
 };
 
@@ -12,6 +13,7 @@ type EnvConfig = {
 
 const config: EnvConfig = {
   baseUrl: '',
+  themeUrl: '',
   isPrd: true // 是否为线上
 };
 switch (import.meta.env.MODE) {
@@ -21,6 +23,7 @@ switch (import.meta.env.MODE) {
      */
     config.isPrd = false;
     config.baseUrl = '/devServer';
+    config.themeUrl = '/devTheme';
     break;
   case 'production':
     /*
@@ -28,6 +31,7 @@ switch (import.meta.env.MODE) {
      */
     config.isPrd = true;
     config.baseUrl = 'https://nutui.jd.com';
+    config.themeUrl = 'https://nutui.jd.com/theme/source';
     break;
 }
 export default config;

+ 4 - 1
src/sites/mobile/App.vue

@@ -8,9 +8,10 @@
   <router-view />
 </template>
 <script lang="ts">
-import { defineComponent, ref, watch, computed } from 'vue';
+import { defineComponent, ref, watch, computed, onMounted } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { isMobile } from '@/sites/assets/util';
+import { useThemeEditor } from '@/sites/assets/util/helper';
 export default defineComponent({
   name: 'app',
   setup() {
@@ -19,6 +20,8 @@ export default defineComponent({
     const route = useRoute();
     const router = useRouter();
 
+    useThemeEditor();
+
     //返回demo页
     const goBack = () => {
       router.back();

+ 5 - 0
vite.config.ts

@@ -18,6 +18,11 @@ export default defineConfig({
         target: 'https://nutui.jd.com',
         changeOrigin: true,
         rewrite: (path) => path.replace(/^\/devServer/, '')
+      },
+      '/devTheme': {
+        target: 'https://nutui.jd.com/theme/source',
+        changeOrigin: true,
+        rewrite: (path) => path.replace(/^\/devTheme/, '')
       }
     }
   },