airyland / vux

Mobile UI Components based on Vue & WeUI
https://vux.li
MIT License
17.59k stars 3.71k forks source link

什么时候支持@vue/cli3.0 #3430

Open saxon-yc opened 5 years ago

saxon-yc commented 5 years ago
sun6143 commented 5 years ago

@zhixueyun9 如果是显示没有按需加载组件,试下这样写

// main.js
// import { LoadingPlugin } from "vux";
// notice: 不要使用解构语法,否则打包会全部打包进去(BUG?)
import ToastPlugin from "vux/src/plugins/toast";
Vue.use(ToastPlugin, { position: "bottom", type: "text", width: "14.6em" });

您好,为什么只有在main.js里不能用解构呢

Gardent commented 5 years ago

@iamziyue 把vux在main.js全局引用就行,不要单独在vue组件里面引用。

我在用@vux/loader 2.0.0-rc4替换vux-loader 1.2.9之后,发现的确是这样的,如果在vue文件引用,就必须用import XHeader from "vux/src/components/x-header"。

不过再替换之前,在main.js不能用import { XHeader } from 'vux',在vue文件里面可以,替换之后就反过来。。。

不能在vue文件使用解构,改起来工作量有点大。

luohong123 commented 5 years ago

image

// 我使用这行代码会出现(如果你看到这一行,说明 vux-loader 配置有问题或者代码书写规范的原因导致无法解析成按需引入组件,会导致打包体积过大。请升级到最新版本 vux-loader,建议开启 eslint(standard)。)的提示
import { Group, Cell } from 'vux'; 

// 使用本行代码后警告消失
import Group from "vux/src/components/group";
import Cell from "vux/src/components/cell";

我的package 如下

 "dependencies": {
    "axios": "^0.19.0",
    "mockjs": "^1.1.0",
    "vue": "^2.6.10",
    "vue-i18n": "^8.15.0",
    "vue-router": "^3.1.3",
    "vux": "^2.9.4"
  },
  "devDependencies": {
    "@babel/core": "^7.6.4",
    "@babel/register": "^7.6.2",
    "@commitlint/cli": "^8.2.0",
    "@commitlint/config-conventional": "^8.2.0",
    "@types/node": "^12.12.3",
    "@vue/cli-plugin-babel": "^4.0.0",
    "@vue/cli-plugin-eslint": "^4.0.0",
    "@vue/cli-service": "^4.0.0",
    "@vux/loader": "^2.0.0-rc4",
    "babel-core": "^6.26.3",
    "babel-eslint": "^10.0.3",
    "chalk": "^2.4.2",
    "chokidar": "^3.2.3",
    "commitizen": "^4.0.3",
    "core-js": "^3.3.6",
    "cz-conventional-changelog": "^3.0.2",
    "cz-customizable": "^6.2.0",
    "eslint": "^5.16.0",
    "eslint-plugin-vue": "^5.0.0",
    "gitmoji-cli": "^2.2.0",
    "husky": "^3.0.9",
    "less-loader": "^5.0.0",
    "node-sass": "^4.13.0",
    "sass-loader": "^8.0.0",
    "script-ext-html-webpack-plugin": "^2.1.4",
    "svg-sprite-loader": "^4.1.6",
    "vue-loader": "^15.7.1",
    "vue-template-compiler": "^2.6.10"
  },

有没有其他的方法统一配置一下,可以支持 import { Group, Cell } from 'vux'; 这种方式引入组件

Riant commented 4 years ago

看了一下其他项目的类似实现,element-ui 使用的是 babel-plugin-components, ant-mobile 以及国内其他几个库使用的也是自己的插件 babel-plugin-import,看了下源码,倒也大概能支持 vux 的按需加载需要。

说一下我的方法哈:

  1. 安装 babel-plugin-import:yarn add -D babel-plugin-import
  2. 修改配置文件 babel.config.js 如下:
    
    const maps = require('./node_modules/vux/src/components/map.json')

module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ], plugins: [ ['import', { libraryName: 'vux', libraryDirectory: 'src/components', style: false, camel2UnderlineComponentName: false, camel2DashComponentName: false, customName: (transformedMethodName) => { return 'vux/' + maps[transformedMethodName] } }] ] }


然后就可以愉快的使用类似 `import { XButton } from 'vux'` 这样的语句按需引入了。

当然目测这个插件仅支持处理 js 组件的 import,不过,倒应该也能解决 80% 以上同学的需求了,而且,我本地 cli 4 没什么问题,cli 3 应该也不会有什么意外。

reset.less 可以在 app.vue 的 style 块用 less 语法引入,也可以直接在 main.js 中通过 import 语句引入,如 `import 'vux/src/styles/reset.less'`;

### theme.less
编辑 `vue.config.js` (如果没有的话,在项目根目录新建一个),大概如下 less 块(@vux/loader 还是需要的 :) ):
``` javascript
const getLessVariables = require('./node_modules/@vux/loader/src/libs/get-less-variables')

module.exports = {
  css: {
    loaderOptions: {
      // sass: {
      //   prependData: `@import "@/assets/style/_setting.scss";`
      // },
      less: {
        modifyVars: getLessVariables('./src/assets/style/theme.less')
      },
    }
  },
}

$t not defined 的问题

如果不用的话,暂时在 main.js 文件添加代码,如下:

const VuxLocals = {
  'cancel': '取消',
  'button_text': '确定',
  'confirm_text': '确定',
  'cancel_text': '取消',
  'week_day_0': '日',
  'week_day_1': '一',
  'week_day_2': '二',
  'week_day_3': '三',
  'week_day_4': '四',
  'week_day_5': '五',
  'week_day_6': '六',
  'loading': '加载中',
  'search.placeholder': '搜索',
  'back_text': '返回',
}

Vue.prototype.$t = t => VuxLocals[t] || t

如有从 webpack 的 template-loader 层面处理的办法,麻烦分享补充哈。

其他如 i18n,yml 那一块的 loader 这个就无能为力了,还要多折腾 vux-loader : )。

Riant commented 4 years ago

$t 在 webpack 层面的处理支持

生命在于折腾,为了实现上边说的 $t 在 webpack 层面的处理,分享一下我从 @vux/loader 里边拆解出来的 template-loader 哈。

  1. 项目根目录(其实其他目录也可以,只要能引用到)新建文件 vux-template-loader2.js,内容如下,基本都是从 @vux/loader/src/template-loader 复制过来的,略有改动:
// Modified by Riant rianthar@qq.com at 2019-11-20
// 主要是为了兼容 vue-cli 3 以及 Webpack 的一些变更对一些方法作一些不严谨的修改
// 未完全测试,不需要 i18n 的话,应该可以直接使用,否则请注意多测试
// 使用: vue.confg.js
// configureWebpack: config => {
//   return {
//     module: {
//       rules: [{
//         enforce: 'pre',
//         test: /\.vue$/,
//         resourceQuery: /^\?vue&type=template/,
//         use: [{loader: './vux-template-loader2', options: {}}]
//       }]
//     }
//   }
// }

'use strict'

const VuxLoaderPath = './node_modules/@vux/loader/'

const _ = require('lodash')
const touch = require('touch')
const utils = require('loader-utils')
const yamlReader = require('js-yaml')
const fs = require('fs')
const path = require('path')
const matchI18nReg = /\$t\('?(.*?)'?\)/g
const parseVirtualComponent = require(VuxLoaderPath + 'src/libs/parse-virtual-component')
const parseXIcon = require(VuxLoaderPath + 'src/libs/parse-x-icon')
const removeTagCode = require(VuxLoaderPath + 'src/libs/getTag').removeTagCode
const reserveTagCode = require(VuxLoaderPath + 'src/libs/getTag').reserveTagCode
// const compareVersions = require('compare-versions')

const i18nParser = require(VuxLoaderPath + 'libs/parse-i18n-function').parse
const getI18nBlockWithLocale = require(VuxLoaderPath + 'libs/get-i18n-block').getWithLocale

const getName = function (path) {
  return path.replace(/\\/g, '/').split('components')[1].replace('index.vue', '').replace(/\//g, '')
}

const getLocals = (options) => {
  const vuxLocalesContent = fs.readFileSync('./node_modules/vux/src/locales/all.yml', 'utf-8')
  return yamlReader.safeLoad(vuxLocalesContent)
}

module.exports = function (source) {
  const _this = this
  this.cacheable()
  // const query = this.query ? utils.parseQuery(this.query) : {}
  let options = utils.getOptions(this)
  let config = this.vux || options.vux || {options: {}, plugins: []}
  const locales = this.vuxLocales || getLocals(options)

  if (this['thread-loader']) {
    const configFile = require.resolve('vux/.config.cache.json')
    config = require(configFile)
  }

  const basename = path.basename(this.resourcePath)
  let isVuxVueFile = this.resourcePath.replace(/\\/g, '/').indexOf('vux/src/components') > -1
  let isVuxVueDemo = this.resourcePath.replace(/\\/g, '/').indexOf('vux/src/demos') > -1
  if (config.options.vuxDev && this.resourcePath.replace(/\\/g, '/').indexOf('src/components') > -1) {
    isVuxVueFile = true
  }
  if (config.options.vuxDev && this.resourcePath.replace(/\\/g, '/').indexOf('src/demos') > -1) {
    isVuxVueDemo = true
  }

  // x-icon
  if (config.options.useVuxUI && source.indexOf('<x-icon') > -1) {
    source = parseXIcon(source, config)
  }

  // v-no-ssr
  if (config.options.ssr) {
    source = removeTagCode(source, 'v-no-ssr')
    source = reserveTagCode(source, 'v-ssr')
  } else {
    source = removeTagCode(source, 'v-ssr')
    source = reserveTagCode(source, 'v-no-ssr')
  }

  /**
   * ======== i18n ========
   */
  let dynamic = false
  let locale = 'zh-CN'
  let vuxFunctionName = '$t'
  let functionName = '$t'
  let staticTranslations = {}
  let langs = ['en', 'zh-CN']
  let staticReplace

  // 如果不设置, dynamic 为false, local 为 zh-CN
  const i18nPluginsMatch = config.plugins.filter(function (one) {
    return one.name === 'i18n'
  })

  if (i18nPluginsMatch.length) {
    dynamic = !i18nPluginsMatch[0].vuxStaticReplace
    locale = i18nPluginsMatch[0].vuxLocale || 'zh-CN'
    vuxFunctionName = i18nPluginsMatch[0].vuxFunctionName || '$t'
    functionName = i18nPluginsMatch[0].functionName || '$t'
    langs = i18nPluginsMatch[0].localeList || langs
    staticTranslations = i18nPluginsMatch[0].staticTranslations || {}
    staticReplace = typeof i18nPluginsMatch[0].staticReplace === 'undefined' ? undefined : i18nPluginsMatch[0].staticReplace
  } else {
    dynamic = false
    locale = 'zh-CN'
    vuxFunctionName = '$t'
  }

  if ((isVuxVueFile) && source.indexOf("$t(") > -1) {
    const name = getName(this.resourcePath)
    if (!dynamic) {
      source = source.replace(matchI18nReg, function (a, b) {
        let key = `vux.${name}.${b}`

        if (a.indexOf('/*') > -1) {
          const start = a.indexOf('/*')
          const end = a.indexOf('*/')
          const str = a.slice(start + 2, end - 1)
          const map = {}
          const list = str.split(',').map(one => {
            one = one.trim()
            const pair = one.split(':').map(one => one.trim())
            map[pair[0]] = pair[1]
          })

          return map[locale]
        }

        if (a.indexOf("'") > -1) { // 用于翻译字符
          return "'" + locales[locale][key] + "'"
        } else { // 用于翻译变量,如 $t(text)
          return b
        }
      })
    } else {
      // dynamic 为 true, 则对于 vux 源码,把 key 加上 prefix
      source = source.replace(matchI18nReg, function (a, b) {
        if (a.indexOf("'") > -1) {
          return a.replace(b, `vux.${name}.${b}`).replace('$t', vuxFunctionName)
        } else {
          return a.replace('$t', vuxFunctionName)
        }
      })
    }
  } else if (!isVuxVueFile && source.indexOf(`${functionName}(`) > -1 && staticTranslations && staticReplace === true) {
    // 对于项目文件进行静态替换
    /**
    let matchI18nReg = new RegExp(`\$${functionName}\('?(.*?)'?\)`, 'g')
    source = source.replace(matchI18nReg, function (a, b) {
      if (a.indexOf("'") > -1) {
        return `${ staticTranslations[b] || b }`
      }
    })
    */
    const rs = getI18nBlockWithLocale({
      code: _this.resourcePath,
      isFile: true,
      locale: locale
    })
    source = i18nParser(source, rs)
  }

  config.plugins.forEach(function (plugin) {

    // template-feature-switch
    /**
    <off feature="false"> show
    <on feature="true"> show

    <off feature="true"> hide
    <on feature="false"> hide
    */

    if (plugin.name === 'template-feature-switch') {
      // replace features
      if (plugin.features && source.indexOf('</on>') > -1) {
        source = parseOnFeature(source, plugin.features)
      }
      if (plugin.features && source.indexOf('</off>') > -1) {
        source = parseOffFeature(source, plugin.features)
      }
    }

    // 非 vux 组件才需要生成语言
    if (!isVuxVueFile && plugin.name === 'i18n' && plugin.extractToFiles) {

      const savePath = path.resolve(config.options.projectRoot, plugin.extractToFiles)

      let format = 'yml'
      if (/\.json$/.test(plugin.extractToFiles)) {
        format = 'json'
      }

      let fileMode = 'all'
      if (plugin.extractToFiles.indexOf('{lang}') !== -1) {
        fileMode = 'single'
      }

      const isDynamic = plugin.staticReplace === false

      if (isDynamic) {
        setTimeout(function () {
          const rawFileContent = fs.readFileSync(_this.resourcePath, 'utf-8')
          const results = rawFileContent.match(/<i18n[^>]*>([\s\S]*?)<\/i18n>/)
          if (results) {
            let attrsMap = {}
            const attrs = results[0].split('\n')[0].replace('<i18n', '')
              .replace('>', '')
              .replace(/"/g, '')
              .replace(/\r/g, '')
              .split(' ')
              .filter(function (one) {
                return !!one
              }).forEach(function (one) {
                let tmp = one.split('=')
                attrsMap[tmp[0]] = tmp[1]
              })

            try {
              const local = yamlReader.safeLoad(results[1])
              const rs = parseI18n(local, langs)
              let finalConfig = {}

              // all and yml format
              if (fileMode === 'all' && format === 'yml') {
                touch.sync(savePath)
                let currentConfig = fs.readFileSync(savePath, 'utf-8')
                if (!currentConfig) {
                  finalConfig = rs.translations
                } else {
                  finalConfig = mergeLang(yamlReader.safeLoad(currentConfig), rs.translations)
                }
                if (!currentConfig || (currentConfig && JSON.stringify(yamlReader.safeLoad(currentConfig)) !== JSON.stringify(finalConfig))) {
                  fs.writeFileSync(savePath, yamlReader.safeDump(finalConfig))
                }
              }

              if (fileMode === 'all' && format === 'json') {
                touch.sync(savePath)
                let currentConfig = fs.readFileSync(savePath, 'utf-8')
                if (!currentConfig) {
                  finalConfig = rs.translations
                } else {
                  finalConfig = mergeLang(JSON.parse(currentConfig), rs.translations)
                }
                if (!currentConfig || (currentConfig && JSON.stringify(currentConfig) !== JSON.stringify(finalConfig))) {
                  fs.writeFileSync(savePath, JSON.stringify(finalConfig, null, 2))
                }
              }

              // single and yml

              if (fileMode === 'single' && format === 'yml') {
                for (let i = 0; i < langs.length; i++) {
                  let lang = langs[i]
                  let savePath = path.resolve(config.options.projectRoot, plugin.extractToFiles).replace('{lang}', lang)
                  touch.sync(savePath)
                  let currentConfig = fs.readFileSync(savePath, 'utf-8')
                  if (!currentConfig) {
                    finalConfig = rs.translations[lang]
                  } else {
                    finalConfig = Object.assign(yamlReader.safeLoad(currentConfig), rs.translations[lang])
                  }
                  if (!currentConfig || (currentConfig && JSON.stringify(yamlReader.safeLoad(currentConfig)) !== JSON.stringify(finalConfig))) {
                    fs.writeFileSync(savePath, yamlReader.safeDump(finalConfig))
                  }
                }
              }

              if (fileMode === 'single' && format === 'json') {

                for (let i = 0; i < langs.length; i++) {
                  let lang = langs[i]
                  let savePath = path.resolve(config.options.projectRoot, plugin.extractToFiles).replace('{lang}', lang)
                  touch.sync(savePath)
                  let currentConfig = fs.readFileSync(savePath, 'utf-8')
                  if (!currentConfig) {
                    finalConfig = rs.translations[lang]
                  } else {
                    finalConfig = Object.assign(JSON.parse(currentConfig), rs.translations[lang])
                  }
                  if (!currentConfig || (currentConfig && JSON.stringify(currentConfig) !== JSON.stringify(finalConfig))) {
                    fs.writeFileSync(savePath, JSON.stringify(finalConfig, null, 2))
                  }
                }
              }

            } catch (e) {
              console.log(e)
              console.log('yml 格式有误,请重新检查')
            }

          }
        })
      }
    }

    // template-parser
    if (plugin.name === 'template-parser') {
      if (plugin.fn) {
        source = plugin.fn.call(_this, source)
      }
      if (plugin.replaceList && plugin.replaceList.length) {
        plugin.replaceList.forEach(function (replacer) {
          source = source.replace(replacer.test, replacer.replaceString)
        })
      }
    }

    if (plugin.name === 'template-string-append') {
      if (new RegExp(plugin.test).test(_this.resourcePath)) {
        var componentName = basename.replace('.vue', '').toLowerCase()
        var string = plugin.fn({
          resourcePath: _this.resourcePath,
          basename: basename
        })
        if (string) {
          source = source.replace(/\s+$/g, '').replace(/\\n/g, '').replace(/<\/div>$/, string + '</div>')
        }
      }
    }
  })

  if (config.options.vuxWriteFile === true) {
    fs.writeFileSync(this.resourcePath + '.vux.html', source)
  }

  source = removeTagCode(source, 'i18n')

  return source
}

function parseOnFeature(content, features) {
  content = content.replace(/<on[^>]*>([\s\S]*?)<\/on>/g, function (tag, text) {
    const key = tag.split('\n')[0].replace('<on', '')
      .replace('>', '')
      .replace(/"/g, '')
      .replace(/\r/g, '')
      .split(' ')
      .filter(function (one) {
        return !!one
      }).map(function (one) {
        let tmp = one.split('=')
        return tmp[1]
      })
    if (features[key] && features[key] === true) {
      // true
      return text
    } else {
      // false
      return ''
    }
  })
  return content
}

function parseOffFeature(content, features) {
  content = content.replace(/<off[^>]*>([\s\S]*?)<\/off>/g, function (tag, text) {
    const key = tag.split('\n')[0].replace('<off', '')
      .replace('>', '')
      .replace(/"/g, '')
      .replace(/\r/g, '')
      .split(' ')
      .filter(function (one) {
        return !!one
      }).map(function (one) {
        let tmp = one.split('=')
        return tmp[1]
      })
    if (!features[key]) {
      // false
      return text
    } else {
      // true
      return ''
    }
  })
  return content
}

function parseI18n(json, langs) {
  langs = langs || []
  if (!langs || !langs.length) {
    for (let i in json) {
      langs = langs.concat(Object.keys(json[i]))
    }
    langs = _.uniq(langs)
  }
  let rs = {}
  for (let i = 0; i < langs.length; i++) {
    let lang = langs[i]

    if (!rs[lang]) {
      rs[lang] = {}
    }

    for (let j in json) {
      rs[lang][j] = json[j][lang] || j
    }

  }
  return {
    langs: langs,
    translations: rs
  }
}

function mergeLang(a, b) {
  for (let i in b) {
    for (let j in b[i]) {
      if (!a[i]) {
        a[i] = b[i]
      }
      a[i][j] = b[i][j]
    }
  }
  return a
}
  1. 修改 vue.config.js 大概如下:
    
    const getLessVariables = require('./node_modules/@vux/loader/src/libs/get-less-variables')

module.exports = { configureWebpack: config => { return { module: { rules: [{ enforce: 'pre', test: /.vue$/, resourceQuery: /^\?vue&type=template/, use: [{loader: './vux-template-loader2', options: {}}] // here }] } } }, css: { loaderOptions: { // 可能有同样使用 scss 的同学需要,可以参考使用哈 // sass: { // prependData: @import "@/assets/style/_setting.scss"; // 注意此处路径 // }, less: { modifyVars: getLessVariables('./src/assets/style/theme.less') // here }, } }, }



这样的话,$t is not defined 之类的问题应该也就解决了;预计 XIcon 应该也可以使用了;其他的特性大家有用到的话,测试了看哈。

这么一折腾的话,其实整个 loader 大概的一些问题也就清楚了,只是其他文件可能还有一些 webpack 及其周边的一些方法库调用方式没有跟上版本可能需要调整,应该可以尝试处理一下 @vux/loader 了。后边看有时间的话试试。

有需要的同学暂时可以将着上述的方法试试哈,如有修改的不严谨的地方,欢迎补充。
DellDi commented 4 years ago

@iamziyue 把vux在main.js全局引用就行,不要单独在vue组件里面引用。

我是老项目版本升级的cli3,我自己没事就折腾一下,暂时不敢发正式。和你出现了一样的问题,请问你解决了吗? main.js里面引入不好办,还有其他方法吗? image 全都是这样的。。。,看了一下午的这片文章了,现在在试@Riant 这老哥的代码

LiangFuzhi commented 4 years ago

cli3升级cli4后,报错 // 我使用这行代码会出现(如果你看到这一行,说明 vux-loader 配置有问题或者代码书写规范的原因导致无法解析成按需引入组件,会导致打包体积过大。请升级到最新版本 vux-loader,建议开启 eslint(standard)。)的提示

flicat commented 4 years ago

看了一圈,每个方案都试了一遍,可以决定放弃这个框架了。以后不要用这种侵入式配置的框架

airyland commented 4 years ago

@flicat 抱歉啊,确实都没有时间做大改动。

flicat commented 3 years ago

3734#issuecomment-723394531

我找到别的办法引入了。

Panway commented 2 years ago

补充一下Riant的回答: https://github.com/airyland/vux/issues/3430#issuecomment-555375644

如果出现如下错误

Syntax Error: ValidationError: Invalid options object. Less Loader has been initialized using an options object that does not match the API schema.

options has an unknown property 'modifyVars'. These properties are valid: object { lessOptions?, additionalData?, sourceMap?, webpackImporter?, implementation? }

若 less-loader 版本< 6.0,则vue.config.js选项为

css: {
    loaderOptions: {
      less: {
          javascriptEnabled: true,
          modifyVars: getLessVariables('./src/assets/style/vux_theme.less')
      }
    }
  },

若 less-loader 版本>= 6.0

css: {
    loaderOptions: {
      less: {
        lessOptions: {
          javascriptEnabled: true,
          modifyVars: getLessVariables('./src/assets/style/vux_theme.less')
        }
      }
    }
  },