vue项目创建总结

543 阅读5分钟

vue项目创建总结

功能概要:

  1. jest用于前端单元测试
  2. eslint用于统一前端的代码风格
  3. jsdoc用于根据注释自动生成文档
  4. cdn公共组件通过cdn引入,现在用的是免费的cdn,可能需要改为自买的cdn服务器(引入文件包含vue-router,vue,element-ui,vuex,axios
  5. gzip通过webpack 生成gzip文件,加载文件可以选择为gzip格式的文件(只有生产才会开启)
  6. 组建自注册、module自动导入、国际化文件自动导入

项目的目录

|-- scripts/                                        --- 脚本文件
	|-- createComponent.js         	            --- 创建组建的文件
	|-- template.js   		            --- 创建组建的模板文件
|-- public/                        	            --- 公共的非打包文件
	|-- index.html			            --- 入口html文件
|-- src/                        	            --- 公共的非打包文件
	|-- layouts/				    --- layout文件的地址
	|-- i18n/				    --- 国际化文件的入口文件
		|-- en.js			    --- 英文国际化文件
		|-- index.js			    --- 国际化入口文件
		|-- zh.js			    --- 中文国际化文件
	|-- components				    --- 公共和全局组建
		|-- global/			    ---全局组建,会自动注册到页面中
		|-- index.js			    --- 自动注册全局组建的js文件,在main.js会引入
	|-- api/
    	|-- request.js				    --- 放置一些全局的api接口调用
	|-- plugins/				    --- 通过vue add 指令安装生成的文件
        |-- store/
            |-- modules				    --- 自动创建局部组建时候,会同步创建以创建名命名的store
            |-- globalStore.js		            --- 全局的状态存储
    	    |-- index.js			    --- 全局组建的创建和自定义组建的创建合并的js,此部分会自动合并
	|-- views/				    --- 局部视图组建的放置的地方(通过脚本创建)
	|-- App.vue				    --- 主view的入口文件,new Vue render函数传入
	|-- main.js				    ---	入口文件
|-- test/					    --- 测试文件
|-- .jsdoc.js					    ---	docs文件
|-- .jsdoc-minami.js			            --- docs生成需要
|-- vue.config.js				    --- webpack 修改webpack配置文件的入口文件
|-- package.json				    --- 项目依赖文件的保存文件
|-- README.md					    --- readme文件

功能介绍

  1. 压缩文件为gzip格式,需要配置vue.config.js,文件的压缩大小在正常压缩的1/3
// vue.config.js文件中添加如下配置文件即可
const CompressionPlugin = require('compression-webpack-plugin');

// Open gzip on line
config
    .plugin('compression')
    .use(CompressionPlugin, {
    asset: '[path].gz[query]',
    algorithm: 'gzip',
    test: new RegExp(`\\.(${['js', 'css'].join('|')})$`),
    threshold: 10240,
    minRatio: 0.8,
    cache: true,
}).tap(() => {});
  1. 由于浏览器并行加载文件的个数是有限的(据观察,chrome是6个),所以要把第三方的模块通过cdn引入
// require common file from cdn at online
      const externals = {
        vue: 'Vue',
        axios: 'Axios',
        'element-ui': 'ELEMENT',
        'vue-router': 'VueRouter',
        vuex: 'Vuex',
      };
      config.externals(externals);
      const cdn = {
        css: [
          '//cdn.bootcss.com/element-ui/2.10.0/theme-chalk/index.css',
        ],
        js: [
          // vue
          '//cdn.bootcss.com/vue/2.6.10/vue.runtime.min.js',
          // vue-router
          '//cdn.bootcss.com/vue-router/3.0.3/vue-router.min.js',
          // vuex
          '//cdn.bootcss.com/vuex/3.0.1/vuex.min.js',
          // axios
          '//cdn.bootcss.com/axios/0.19.0/axios.min.js',
          // element-ui js
          '//cdn.bootcss.com/element-ui/2.10.0/index.js',
        ],
      };
      config.plugin('html')
        .tap((args) => {
          args[0].cdn = cdn; // eslint-disable-line
          return args;
        });
    }
  },

注意 : 在项目中引入文件需要按照上面的规则:import Vue from 'vue',参照externals常量的设置

通过上面这样做,还没有办法引入到文件中,执行应该会报错,接下来介绍加载文件的正确姿势

<% if (process.env.NODE_ENV === 'production') { %>
    <% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
    	<link rel="stylesheet" href="<%=css%>" as="style">
    <% } %>
    <% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
    	<script src="<%=js%>"></script>
    <% } %>
<% } %>

解释: 上面代码是根据cdn对象生成引用,这个做了一个限制,只有生产的时候才会这样加载。

  1. 完成https项目的添加,(在vue.config.js)
// 引入node模块 
const fs = require('fs');
const path = require('path');

devServer: {
    https: {
      key: fs.readFileSync(path.join(__dirname, './cert/privatekey.pem')),
      cert: fs.readFileSync(path.join(__dirname, './cert/certificate.pem')),
    },
  },
  1. jsdoc的引入,需要安装以下包
npm i  jsdoc jsdoc-vuejs vue-template-compiler@2.6.10 minami -D

jsdoc 配置文件有以下两个.jsdoc.js.jsdoc-minami.js 这两个文件

  • 在根目录下创建文件.jsdoc.js

    module.exports = {
      plugins: ['node_modules/jsdoc-vuejs'],
      source: {
        include: [
          'src/components/',
          'README.md',
          'scripts/',
        ],
        includePattern: '\\.(vue|js)$',
        excludePattern: '(node_modules/|docs)',
      },
      opts: {
        encoding: 'utf8',
      },
    };
    
  • 在项目目录下面创建.jsdoc-minami.js

    const config = Object.assign({}, require('./.jsdoc'));
    
    config.opts.destination = 'docs';
    config.opts.template = './node_modules/minami';
    
    module.exports = config;
    
  • package.json 文件中添加如下scripts

"docs": "jsdoc -d docs -c .jsdoc-minami.js",
  • 注释的写法包含两部分

    • 可以下面这样加

       /**
       * @vue-prop {Number} [step=1] - Step
       * @vue-data {Number} counter - Current counter's value
       * @vue-computed {String} message
       * @vue-event {Number} increment - Emit counter's value after increment
       * @vue-event {Number} decrement - Emit counter's value after decrement
       * @vue-methods {Number} decrement - Emit counter's value after decrement
       */
      
    • 参照jsdoc文档

  1. 通过脚本生成组建,需要两个文件,一个是脚本文件,一个是模板文件

    • 创建脚本文件

      // scripts/createComponent.js
      
      const chalk = require('chalk')
      const path = require('path')
      const fs = require('fs')
      const resolve = (...file) => path.resolve(__dirname, ...file)
      const log = message => console.log(chalk.magenta(`${message}`))
      const successLog = message => console.log(chalk.green(`${message}`))
      const errorLog = error => console.log(chalk.red(`${error}`))
      const template = require('./template')
      /**
       * 创建文件
       *
       * @methods CreatFile
       * @param {string} path - 文件的路径
       * @param {string} data - 文件的内容
       */
      const CreatFile = (path, data) => {
          if (fs.existsSync(path)) {
              errorLog(`${path}文件已存在`)
              return
          }
          return new Promise((resolve, reject) => {
              fs.writeFile(path, data, 'utf8', err => {
                  if (err) {
                      errorLog(err.message)
                      reject(err)
                  } else {
                      resolve(true)
                  }
              })
          })
      }
      log('请输入要生成的组件名称、如需生成全局组件,请加 global/ 前缀')
      let componentName = ''
      /**
       * 监听用户输入,回车键会触发监听事件(只可以在规定目录下面创建一级目录,不允许嵌套多级目录创建)
       * 
       * @event process.stdin#data 
       */
      process.stdin.on('data', async chunk => {
          const inputName = String(chunk).trim().toString()
          // 定义基础路径
          let baseDirectory = '../src/'
      
          const isGlobal = inputName.startsWith('global/') ? true : false
          const tempArr = inputName.split('/');
          // 防止局部组件多级嵌套
          if (!isGlobal && tempArr.length > 1) {
              // 表示创建的为全局组件,默认全局组件在 components/global文件夹下面
              errorLog(`${inputName}非法,局部组件不能包含/,请重新输入`)
              return
          }
          // 防止global多级嵌套
          if (isGlobal && (tempArr.length > 2 || tempArr[1] == '')) {
              // 表示创建的为全局组件,默认全局组件在 components/global文件夹下面
              errorLog(`${inputName}非法,全局组件只能包含1个/,请重新输入`)
              return
          }
      
          // 组件的首字母变为大写
          let changeComponentFolder
          if (isGlobal) {
              tempArr[1] = tempArr[1].replace(tempArr[1][0], tempArr[1][0].toUpperCase())
              changeComponentFolder = tempArr.join("/");
          } else {
              changeComponentFolder = inputName.replace(inputName[0], inputName[0].toUpperCase());
          }
          const componentDirectory = isGlobal ? resolve(baseDirectory, 'components/', changeComponentFolder) : resolve(baseDirectory, 'views/', changeComponentFolder);
      
          // 判断文件夹是否存在,如果存在,则return,如果不存在则创建文件
          const hasComponentDirectory = fs.existsSync(componentDirectory)
          if (hasComponentDirectory) {
              errorLog(`${changeComponentFolder}组件目录已存在,请重新输入`)
              return
          } else {
              log(`正在生成 component 目录 ${componentDirectory}`)
              await dotExistDirectoryCreate(componentDirectory)
              log(`正在生成 component 目录 ${componentDirectory}/lang`)
              await dotExistDirectoryCreate(`${componentDirectory}/lang`)
          }
      
          try {
              if (changeComponentFolder.includes('/')) {
                  const inputArr = changeComponentFolder.split('/')
                  componentName = inputArr[inputArr.length - 1]
              } else {
                  componentName = changeComponentFolder
              }
              await Promise.all(Object.keys(template).map(key => {
                  if (!((key === 'store' || key === 'Api') && isGlobal)) {
                      const obj = template[key];
                      // 获取路径
                      let dirpath;
                      if (typeof obj.path == "function") {
                          dirpath = resolve(componentDirectory, obj.path(componentName, isGlobal));
                      } else {
                          dirpath = resolve(componentDirectory, obj.path);
                      }
                      log(`正在生成 ${dirpath} 文件 `)
                      if (typeof obj.content == "function") {
                          return CreatFile(dirpath, obj.content(componentName, isGlobal))
                      } else {
                          return CreatFile(dirpath, obj.content)
                      }
                  }
              }))
              successLog('生成成功')
          } catch (e) {
              errorLog(e.message)
          }
      
          process.stdin.emit('end')
      })
      
      /**
       * 创建完成组件需要关闭进程
       * 
       * @event process.stdin#end 
       */
      process.stdin.on('end', () => {
          log('exit')
          process.exit()
      })
      
      /**
       * 调用mkdirs创建问价夹
       *
       * @methods dotExistDirectoryCreate
       * @param {string} directory - 文件的路径
       * @returns {Promise} - 文件夹创建的结果
       */
      function dotExistDirectoryCreate(directory) {
          return new Promise((resolve) => {
              mkdirs(directory, function () {
                  resolve(true)
              })
          })
      }
      
      /**
       * 判断文件夹是否存在,如果存在执行callback,不存在创建文件夹
       *
       * @methods mkdirs
       * @param {string} directory - 文件夹目录
       */
      function mkdirs(directory, callback) {
          var exists = fs.existsSync(directory)
          if (exists) {
              callback()
          } else {
              mkdirs(path.dirname(directory), function () {
                  fs.mkdirSync(directory)
                  callback()
           })
          }
         }
      
  • 创建模板文件

    // scripts/template.js
    module.exports = {
      vueTemplate: {
        path: 'Index.vue',
        tag: true, // 表示在创建的路径下面用
        content: (componentName,isGlobal) => {
          let content = isGlobal?
          {reportContent:'',computedContent:''}:{reportContent:`// import { createNamespacedHelpers } from 'vuex'
    
    // 此处可以把局部的变为当作全局使用,示例在下面
    // const { mapState} = createNamespacedHelpers('${componentName.toLowerCase()}')`,
    computedContent:`computed: {
      // ...mapState({
      //   num: state => state.num
      // })
    },`
        
        };
        return `<template>
      <div class="${componentName}"> ${componentName}组件</div>
    </template>
    <script>
    // import { exampleApi } from './api/${componentName.toLowerCase()}'
    ${content.reportContent}
    export default {
      name: "${componentName}",
      ${content.computedContent}
      mounted() {}
    };
    </script>
    <style lang="scss" scoped>
    .${componentName} {
    }
    </style>`
        }
      },
      entryTemplate: {
        path: 'index.js',
        flag: true,
        content: `import Index from './Index.vue'
    export default Index`
      },
      Api:{
        path: (componentName) => {
          const baseUrl =`../../api/`
          return `${baseUrl}${componentName&&componentName.toLowerCase()}.js`
        },
        flag: true,
        content: `//import request from '@/api/request';
    
    // example
    // export async function exampleApi(id){
    //    let res = await request.get('url');
    //    return res;
    // }`
      },
      store:{
        path: (componentName,isGlobal) => {
          const baseUrl = isGlobal?`../../../store/global/`:'../../store/modules/'
          return `${baseUrl}${componentName&&componentName.toLowerCase()}.js`
        },
        flag: true,
        content: `// export default {
    //   namespaced: true, // 模块内的状态已经是嵌套的了,使用 namespaced 属性不会对其产生影响
    
    //   state: {
    //     num: 1,
    //   },
    //   getters: {},
    //   actions: {},
    //   mutations: {},
    // };`
      },
      langEn:{
        path: 'lang/en.js',
        flag: true,
        content: `// export default {
    //    key: value
    // };` 
      },
      langZh:{
        path: 'lang/zh.js',
        flag: true,
        content: `// export default {
    //    key: value
    // };` 
      }
    }
    
  • 添加命令:(package.json)

"newcomp": "node ./scripts/createComponent"

为了代码分隔一致性,目录的简洁做了以下优化

  • 通过npm run newcomp交互式的创建组件不要自己创建
  1. 创建全局组件需要global/**形式创建,创建成功之后可以看控制台目录,类似这种:

    |-- src/components/global
          |-- **
              |-- Index.vue
              |-- index.js
              |-- langs/
                     |-- en.js
                     |-- zh.js
    
  2. 创建局部组件需要 ** 形式即可,创建的成功之后文件类似于下面:

      |-- src/components/views
        	|-- **
                  |-- Index.vue
                  |-- index.js
                  |-- langs/
                        |-- en.js
                        |-- zh.js
    
  3. 全局组建的自动导入(利用了webpack 的require.context api)

    • 以自动引入store为例(引入全局组建、国际化文件的合并同理)
    const store = {};
    const storeContext = require.context('./modules', true, /\.js$/);
    storeContext.keys().forEach((storeModule) => {
      if (/([\w]*)\.js$/.test(storeModule)) {
        const storeConfig = storeContext(storeModule);
        /**
        * 兼容 import export 和 require module.export 两种规范
        */
        const curStore = storeConfig.default || storeConfig;
        if (Object.keys(curStore).length !== 0) {
          store[RegExp.$1] = curStore;
        }
      }
    });
    

源码请看github: vue-project

参考:vue-cli3 项目从搭建优化到docker部署