vue-cli+vant+webpack搭建移动端开发环境

1,080 阅读4分钟

创建项目

vue create vue-mobile-app

手动选择特性

1.Please pick a preset: 请选择一个预设

ts-pritter-es-iview (vue-router, vuex, node-sass, babel, typescript, eslint, unit-mocha)
default (babel, eslint) > Manually select features

2.Please pick a preset: Manually select features? Check the features needed for your project: 请选择一个预设:手动选择功能? 检查项目所需的功能:

(*) Babel
( ) TypeScript
(*) Progressive Web App (PWA) Support
(*) Router
(*) Vuex
(*) CSS Pre-processors
(*) Linter / Formatter
( ) Unit Testing
( ) E2E Testing

3.Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) 路由模式选择

4.Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): 选择一个CSS预处理器(默认情况下支持PostCSS,Autoprefixer和CSS模块): Sass/SCSS (with dart-sass) Sass/SCSS (with node-sass) √ Less Stylus

5.Pick a linter / formatter config: 选择一个linter / formatter配置:

ESLint with error prevention only
ESLint + Airbnb config
ESLint + Standard config √
ESLint + Prettier

配置 Eslint 检查规则

根目录新建 .eslintrc.js

// # eslint 配置项
module.exports = {
  root: true,
  env: {
    node: true
  },
  extends: ['eslint:recommended', 'plugin:vue/essential', '@vue/standard'],
  parserOptions: {
    parser: 'babel-eslint'
  },
  rules: {
    'no-console': 'off',
    'accessor-pairs': [2, { setWithoutGet: true }], // 强制 getter 和 setter 在对象中成对出现
    'arrow-spacing': [2, { before: true, after: true }], // 强制箭头函数的箭头前后使用一致的空格
    'block-spacing': [2, 'always'], // 禁止或强制在代码块中开括号前和闭括号后有空格
    'brace-style': [2, '1tbs', { allowSingleLine: true }], // 强制在代码块中使用一致的大括号风格
    camelcase: [2, { properties: 'always' }], // 强制使用骆驼拼写法命名约定
    'comma-dangle': [2, 'never'], // 要求或禁止末尾逗号
    'comma-spacing': [2, { before: false, after: true }], // 强制在逗号周围使用空格
    'comma-style': [2, 'last'], // 要求逗号放在数组元素、对象属性或变量声明之后,且在同一行
    'constructor-super': 2, // 要求在构造函数中有 super() 的调用
    curly: 2, // 要求遵循大括号约定
    'dot-location': [2, 'property'], // 要求点操作符和属性放在同一行
    'eol-last': 2, // 要求在非空文件末尾至少存在一行空行
    eqeqeq: [2, 'always'], // 强制在任何情况下都使用 === 和 !==
    'generator-star-spacing': [2, { before: false, after: true }], // 强制 generator 函数中 * 号后有空格
    'handle-callback-err': [2, '^(err|error)$'], // 强制回调错误处理
    indent: [2, 2, { SwitchCase: 1 }], // 强制使用一致的缩进
    'jsx-quotes': [2, 'prefer-single'], // 强制在 JSX 属性中一致地使用单引号
    'key-spacing': [2, { beforeColon: false, afterColon: true }], // 强制在对象字面量的属性中键和值之间使用一致的间距
    'keyword-spacing': [2, { before: true, after: true }], // 关键字周围空格的一致性
    'new-cap': [2, { newIsCap: true, capIsNew: false }], // 要求构造函数首字母大写
    'new-parens': 2, // 要求调用无参构造函数时带括号
    'no-array-constructor': 2, // 禁止使用 Array 构造函数
    'no-caller': 2, // 禁用 caller 或 callee
    'no-class-assign': 2, // 不允许修改类声明的变量
    'no-cond-assign': 2, // 禁止条件表达式中出现赋值操作符
    'no-const-assign': 2, // 禁止修改 const 声明的变量
    'no-control-regex': 2, // 禁止在正则表达式中使用控制字符
    'no-delete-var': 2, // 禁止删除变量
    'no-dupe-args': 2, // 禁止 function 定义中出现重名参数
    'no-dupe-class-members': 2, // 禁止类成员中出现重复的名称
    'no-dupe-keys': 2, // 禁止对象字面量中出现重复的 key
    'no-duplicate-case': 2, // 禁止出现重复的 case 标签
    'no-empty-character-class': 2, // 禁止在正则表达式中使用空字符集
    'no-empty-pattern': 2, // 禁止使用空解构模式
    'no-eval': 2, // 禁用 eval()
    'no-ex-assign': 2, // 禁止对 catch 子句的参数重新赋值
    'no-extend-native': 2, // 禁止扩展原生对象
    'no-extra-bind': 2, // 禁止不必要的 .bind() 调用
    'no-extra-boolean-cast': 2, // 禁止不必要的布尔转换
    'no-extra-parens': 2, // 禁止不必要的括号
    'no-fallthrough': 2, // 禁止 case 语句落空
    'no-floating-decimal': 2, // 禁止数字字面量中使用前导和末尾小数点
    'no-func-assign': 2, // 禁止对 function 声明重新赋值
    'no-implied-eval': 2, // 禁止使用类似 eval() 的方法
    'no-inner-declarations': [2, 'both'], // 禁止在嵌套的块中出现变量声明或 function 声明
    'no-invalid-regexp': 2, // 禁止 RegExp 构造函数中存在无效的正则表达式字符串
    'no-irregular-whitespace': 2, // 禁止在字符串和注释之外不规则的空白
    'no-iterator': 2, // 禁用 __iterator__ 属性
    'no-label-var': 2, // 不允许标签与变量同名
    'no-labels': [2, { allowLoop: false, allowSwitch: false }], // 禁用标签语句
    'no-lone-blocks': 2, // 禁用不必要的嵌套块
    'no-mixed-spaces-and-tabs': 2, // 禁止空格和 tab 的混合缩进
    'no-multi-spaces': 2, // 禁止使用多个空格
    'no-multi-str': 2, // 禁止使用多行字符串
    'no-multiple-empty-lines': [2, { max: 1 }], // 不允许多个空行
    'no-global-assign': 2, // 禁止对原生对象或只读的全局对象进行赋值
    'no-unsafe-negation': 2, // 禁止在in表达式中否定左操作数
    'no-new-object': 2, // 禁用 Object 的构造函数
    'no-new-require': 2, // 禁止调用 require 时使用 new 操作符
    'no-new-symbol': 2, // 禁止 Symbolnew 操作符和 new 一起使用
    'no-new-wrappers': 2, // 禁止对 String,Number 和 Boolean 使用 new 操作符
    'no-obj-calls': 2, // 禁止把全局对象作为函数调用
    'no-octal': 2, // 禁用八进制字面量
    'no-octal-escape': 2, // 禁止在字符串中使用八进制转义序列
    'no-path-concat': 2, // 禁止对 __dirname 和 __filename 进行字符串连接path.jonin() 和 path.reslove() 非常适合替换字符串拼接来创建文件和目录路径。
    'no-proto': 'error',
    'no-redeclare': 2, // 禁止多次声明同一变量
    'no-regex-spaces': 2, // 禁止正则表达式字面量中出现多个空格
    'no-return-assign': [2, 'except-parens'], // 禁止出现赋值语句,除非使用括号把它们括起来。
    'no-self-assign': 2, // 禁止自我赋值
    'no-self-compare': 2, // 禁止自身比较
    'no-sequences': 2, // 禁用逗号操作符
    'no-shadow-restricted-names': 2, // 禁止将标识符定义为受限的名字
    'func-call-spacing': [2, 'never'], // 要求或禁止在函数标识符和其调用之间有空格
    'no-sparse-arrays': 2, // 禁用稀疏数组
    'no-this-before-super': 2, // 禁止在构造函数中,在调用 super() 之前使用 this 或 super
    'no-throw-literal': 2, // 禁止抛出异常字面量
    'no-trailing-spaces': 2, // 禁用行尾空白
    'no-undef': 2, // 禁用未声明的变量,除非它们在 /*global */ 注释中被提到
    'no-undef-init': 2, // 禁止将变量初始化为 undefined
    'no-unexpected-multiline': 2, // 禁止出现令人困惑的多行表达式
    'no-unmodified-loop-condition': 2, // 禁用一成不变的循环条件
    'no-unneeded-ternary': [2, { defaultAssignment: false }], // 禁止可以在有更简单的可替代的表达式时使用三元操作符
    'no-unreachable': 2, // 禁止在return、throw、continuebreak 语句之后出现不可达代码
    'no-unsafe-finally': 2, // 禁止在 finally 语句块中出现控制流语句
    'no-unused-vars': [2, { vars: 'all', args: 'none' }], // 禁止出现未使用过的变量,不检查参数
    'no-useless-call': 2, // 禁止不必要的 .call() 和 .apply()
    'no-useless-computed-key': 2, // 禁止在对象中使用不必要的计算属性
    'no-useless-constructor': 2, // 禁用不必要的构造函数
    'no-useless-escape': 2, // 禁用不必要的转义字符
    'no-whitespace-before-property': 2, // 禁止属性前有空白
    'no-with': 2, // 禁用 with 语句
    'one-var': [2, { initialized: 'never' }], // 要求每个作用域的初始化的变量有多个变量声明
    'operator-linebreak': [
      2,
      'after',
      {
        overrides: {
          '?': 'before',
          ':': 'before'
        }
      }
    ], // 强制操作符使用一致的换行符风格
    'padded-blocks': [2, 'never'], // 禁止块语句和类的开始或末尾有空行
    quotes: [2, 'single', { avoidEscape: true, allowTemplateLiterals: true }], // 强制使用一致的反勾号、双引号或单引号
    semi: [2, 'never'], // 禁止在语句末尾使用分号
    'semi-spacing': [2, { before: false, after: true }], // 强制分号之前和之后使用一致的空格
    'space-before-blocks': [2, 'always'], // 强制在块之前使用一致的空格
    'space-before-function-paren': ['error', 'always'], // 强制在 function的左括号之前使用一致的空格
    'space-in-parens': [2, 'never'], // 强制在圆括号内使用一致的空格
    'space-infix-ops': 2, // 要求操作符周围有空格
    'space-unary-ops': [2, { words: true, nonwords: false }], // 强制在一元操作符前后使用一致的空格
    'spaced-comment': [
      2,
      'always',
      {
        markers: [
          'global',
          'globals',
          'eslint',
          'eslint-disable',
          '*package',
          '!',
          ','
        ]
      }
    ], // 强制在注释中 // 或 /* 使用一致的空格
    'template-curly-spacing': [2, 'never'], // 禁止花括号内出现空格
    'use-isnan': 2, // 要求使用 isNaN() 检查 NaN
    'valid-typeof': 2, // 强制 typeof 表达式与有效的字符串进行比较
    'wrap-iife': [2, 'any'], // 要求 立即执行函数 使用括号括起来
    'yield-star-spacing': [2, 'both'], // 强制在 yield* 表达式中 * 周围使用空格
    yoda: [2, 'never'], // 禁止 “Yoda” 条件
    'prefer-const': 2, // 要求使用 const 声明那些声明后不再被修改的变量
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
    'object-curly-spacing': [2, 'always', { objectsInObjects: false }], // 要求花括号内有空格 (除了 {})禁止以对象元素开始或结尾的对象的花括号中有空格
    'array-bracket-spacing': [2, 'never'], // 强制数组方括号中使用一致的空格
    'vue/jsx-uses-vars': 2, // 防止JSX中使用的变量被标记为未使用
    'vue/no-parsing-error': [2, { 'x-invalid-end-tag': false }]
  }
}

vue.config.js配置

根目录创建 vue.config.js 生成环境开启了cdn加速, TerserPlugin插件开启多线程压缩

const path = require('path')
const TerserPlugin = require('terser-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
  .BundleAnalyzerPlugin

const port = 8090 // dev port
const isProduction = process.env.NODE_ENV === 'production'
function resolve (dir) {
  return path.join(__dirname, dir)
}
const externals = {
  'vue': 'Vue',
  'vue-router': 'VueRouter',
  'vuex': 'Vuex',
  'vant': 'vant',
  'axios': 'axios'
}
const cdn = {
  css: ['https://cdn.jsdelivr.net/npm/vant@2.0.5/lib/index.css'],
  js: [
    'https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js',
    'https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.1.3/vue-router.min.js',
    'https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.1/axios.min.js',
    'https://cdnjs.cloudflare.com/ajax/libs/vuex/3.1.2/vuex.min.js',
    'https://cdn.jsdelivr.net/npm/vant@2.0.5/lib/vant.min.js'
  ]
}
module.exports = {
  publicPath: './', // router hash 模式使用
  outputDir: 'dist',
  assetsDir: 'static',
  lintOnSave: !isProduction,
  parallel: require('os').cpus().length > 1, // 构建时开启多进程处理babel编译
  productionSourceMap: false, // 是否为生产环境构建生成 source map
  devServer: {
    port: port,
    open: true,
    overlay: {
      warnings: false,
      errors: true
    },
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:3000',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    },
    before: app => { }
  },
  css: {
    // https://cli.vuejs.org/zh/guide/css.html#css-modules
    sourceMap: false, // 是否在构建样式地图,false将提高构建速度
    loaderOptions: {
      // 给 sass-loader 传递选项
      sass: {
        prependData: `@import "@/styles/variables.scss";` // 向所有 scss 样式传入共享的全局变量
      }
    }
  },
  // webpack 自定义配置
  configureWebpack: config => {
    if (isProduction) {
      // 生产环境配置
      Object.assign(config, {
        externals: externals
      })
      config.plugins.push(new BundleAnalyzerPlugin())
      config.plugins.push(
        new TerserPlugin({
          // 是否开启多线程
          parallel: true,
          test: /\.js(\?.*)?$/i,
          terserOptions: {
            // 去除打印
            compress: {
              'warnings': false,
              'drop_console': true,
              'drop_debugger': true,
              'pure_funcs': ['console.log']
            }
          }
        })
      )
    } else {
      // 开发环境配置
    }
  },
  chainWebpack: config => {
    config.plugins.delete('preload') // TODO: need test
    config.plugins.delete('prefetch') // TODO: need test
    // 添加别名设置
    config.resolve.alias
      .set('@', resolve('src'))
      .set('@api', resolve('src/api'))
      .set('@store', resolve('src/store'))
      .set('@views', resolve('src/views'))
      .set('@utils', resolve('src/utils'))
      .set('@styles', resolve('src/styles'))
      .set('@mixins', resolve('src/mixins'))
      .set('@assets', resolve('src/assets'))
      .set('@components', resolve('src/components'))
    config.when(isProduction, config => {
      //  添加CDN参数到htmlWebpackPlugin配置中, 详见public/index.html
      config.plugin('html').tap(args => {
        args[0].cdn = cdn
        return args
      })
      config
        .plugin('ScriptExtHtmlWebpackPlugin')
        .after('html')
        .use('script-ext-html-webpack-plugin', [
          {
            // `runtime` must same as runtimeChunk name. default is `runtime`
            inline: /runtime\..*\.js$/
          }
        ])
        .end()
      config.optimization.splitChunks({
        chunks: 'all',
        cacheGroups: {
          libs: {
            name: 'chunk-libs',
            test: /[\\/]node_modules[\\/]/,
            priority: 10,
            chunks: 'initial' // only package third parties that are initially dependent
          },
          vant: {
            name: 'chunk-vant', // split vant into a single package
            priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
            test: /[\\/]node_modules[\\/]_?vant(.*)/ // in order to adapt to cnpm
          },
          commons: {
            name: 'chunk-commons',
            test: resolve('src/components'), // can customize your rules
            minChunks: 3, //  minimum common number
            priority: 5,
            reuseExistingChunk: true
          }
        }
      })
      config.optimization.runtimeChunk('single')
    })
  }
}

移动端自适应配置

安装依赖:npm install postcss-aspect-ratio-mini postcss-cssnext postcss-px-to-viewport postcss-viewport-units postcss-write-svg cssnano cssnano-preset-advanced -D

新建 postcss.config.js 更多配置参考 github.com/michael-cin…

module.exports = ({ file, options, env }) => {
  const isVant = !!(file.dirname.includes('node_modules') && file.dirname.includes('vant'))
  const vWidth = isVant ? 375 : 750
  const vHeight = isVant ? 667 : 1334
  const viewportUnitsConfig = {
    'postcss-viewport-units': {}
  }
  const pluginsConfig = {
    plugins: {
      'postcss-aspect-ratio-mini': {},
      'postcss-write-svg': { utf8: false }, // 处理移动端1px的问题
      'postcss-cssnext': {}, // 让我们可以使用css未来特性 会对这些特性做相关的兼容性处理
      'postcss-px-to-viewport': {
        viewportWidth: vWidth, // 视窗的宽度,对应的是我们设计稿的宽度,一般是750
        viewportHeight: vHeight, // 视窗的高度,根据750设备的宽度来指定,一般指定1334,也可以不配置
        unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
        viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw
        selectorBlackList: ['.ignore', '.hairlines'], // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
        minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
        mediaQuery: false // 允许在媒体查询中转换`px`
      },
      'cssnano': { // cssnano主要用来压缩和清理CSS代码。在Webpack中,cssnano和css-loader捆绑在一起,所以不需要自己加载它
        preset: 'advanced', // 安装cssnano-preset-advanced
        autoprefixer: false, // 重复调用 禁用autoprefixer
        'postcss-zindex': false // 禁用postcss-zindex 否则z-index 的值会被重置为1
      }
    }
  }
  if (isVant) {
    return pluginsConfig
  } else {
    return Object.assign(pluginsConfig, viewportUnitsConfig)
  }
}

项目地址:gitee.com/shdulu/vue-…