vue项目创建总结
功能概要:
- jest用于前端单元测试
- eslint用于统一前端的代码风格
- jsdoc用于根据注释自动生成文档
- cdn公共组件通过cdn引入,现在用的是免费的cdn,可能需要改为自买的cdn服务器(引入文件包含vue-router,vue,element-ui,vuex,axios)
- gzip通过webpack 生成gzip文件,加载文件可以选择为gzip格式的文件(只有生产才会开启)
- 组建自注册、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文件
功能介绍
- 压缩文件为
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(() => {});
- 由于浏览器并行加载文件的个数是有限的(
据观察,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对象生成引用,这个做了一个限制,只有生产的时候才会这样加载。
- 完成
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')),
},
},
jsdoc的引入,需要安装以下包
npm i jsdoc jsdoc-vuejs vue-template-compiler@2.6.10 minami -D
jsdoc 配置文件有以下两个.jsdoc.js 和.jsdoc-minami.js 这两个文件
-
在根目录下创建文件
.jsdoc.jsmodule.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.jsconst 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文档
-
-
通过脚本生成组建,需要两个文件,一个是脚本文件,一个是模板文件
-
创建脚本文件
// 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交互式的创建组件不要自己创建
-
创建全局组件需要
global/**形式创建,创建成功之后可以看控制台目录,类似这种:|-- src/components/global |-- ** |-- Index.vue |-- index.js |-- langs/ |-- en.js |-- zh.js -
创建局部组件需要 ** 形式即可,创建的成功之后文件类似于下面:
|-- src/components/views |-- ** |-- Index.vue |-- index.js |-- langs/ |-- en.js |-- zh.js -
全局组建的自动导入(利用了webpack 的
require.contextapi)- 以自动引入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