现在我们做项目基本都是使用脚手架来初始化项目,不得不说脚手架减少了我们很多工作量。但是我们还需要根据项目的特点,对配置文件进行修改,从而使得我们的开发更加方便快速,甚至还可以做一些性能上的优化。本文就总结一下我使用 vue 脚手架创项目之后所做的一些修改。
首先看一下我使用 vue 脚手架创建项目时所选的配置。采用的是typescript
+vue2
+class-component
+scss
开发模式。
在项目中,我们会用到很多小图片。这个时候我们需要借助webpack-spritesmith
这个插件把这些小图片合并成一张大的精灵图。这样做的好处如下:
background-image
这些东西,因为webpack-spritesmith
这个插件已经帮我们做好了。而且只要小图片的命名符合规范,对应的类名就可以自动实现hover
效果。这样就可以加快我们的开发效率了。配置步骤如下:
webpack-spritesmith
npm i webpack-spritesmith -D
vue.config.js
const SpritesmithPlugin = require("webpack-spritesmith");
const path = require("path");
module.exports = {
configureWebpack: (config) => {
config.plugins.push(
new SpritesmithPlugin({
src: {
// 小图片的存放路径
cwd: path.resolve(__dirname, "./src/assets/images/icons"),
// 后缀名为.png的图片将会被合并成一张大的雪碧图
glob: "*.png",
},
target: {
// 生成出来的雪碧图的存放路径
image: path.resolve(__dirname, "./src/assets/images/sprites.png"),
// 生成出来雪碧图样式存放路径
css: [path.resolve(__dirname, "./src/assets/css/sprites.scss")],
},
apiOptions: {
// 生成出来的雪碧图相对于生成出来雪碧图样式的路径
cssImageRef: "../images/sprites.png",
},
spritesmithOptions: {
// 排版方向,自上而下排版
algorithm: "top-down",
},
})
);
},
};
首先新建一个.scss
样式文件,然后引入到入口文件中。我们将在这个新建的.scss
样式文件中重写雪碧图的样式。
// 引入生成出来的雪碧图样式文件
@import "./sprites.scss";
@mixin spritesRewrite($sprites) {
@each $sprite in $sprites {
// 小图片的名称
$sprite-name: nth($sprite, 10);
// hover效果的小图片名称
$sprite-hover-name: #{$sprite-name}_hover;
.icon-#{$sprite-name} {
display: inline-block;
@include sprite($sprite);
// 判断是否存在对应的hover小图片
@if variable-exists($sprite-hover-name) {
&:hover {
@extend .icon-#{$sprite-hover-name};
}
}
}
}
}
@include spritesRewrite($spritesheet-sprites);
首先要先在上面配置文件中的icons
目录下发小图片。比如现在目录下有文件名为star.png
和star_hover.png
的 png 图片,star_hover.png
图片是鼠标悬浮在star.png
图片上面所需要展现出来的图片。我们只需要icon-${文件名}
这样使用即可
<span class="icon-star">span>
现在,上面的 span 元素就会显示出一个对应的小图片了,并且鼠标悬浮上去还会有 hover 图片的效果了
注意:如果不存在${文件名}_hover.png
格式名称的图片,鼠标悬浮上去的时候是没有 hover 效果的
在项目中我们可能会使用一些 scss 变量,比如主题色,字体大小,背景色等变量,这些变量在很多地方都用到了。为了减少在文件中的引入次数,我们借助style-resources-loader
和vue-cli-plugin-style-resources-loader
将这些 scss 变量注册为全局变量。这样就不用在使用这些 scss 变量的时候去引入 scss 变量文件,大大地提高了我们的开发效率。
npm i style-resources-loader vue-cli-plugin-style-resources-loader -D
vue.config.js
const path = require("path");
module.exports = {
pluginOptions: {
/**
* 全局 scss 变量配置
* @param {string | string[]} patterns 你想要全局注册的 scss 变量
*/
"style-resources-loader": {
preProcessor: "scss",
patterns: [
path.resolve(__dirname, "src/assets/styles/variables.scss"),
path.resolve(__dirname, "src/assets/styles/mixins/*.scss"),
],
},
},
};
注意:这里我强烈建议大家把一些主题色,标题颜色,边框颜色,字号大小等抽取出来放在一个文件中,这样可以方便后期管理。如果你想要做主题变换,这就相当的方便了。同时还有一些背景图,我建议大家也把这些背景图统一放在这里管理
我们开发项目的时候,一些库的版本号基本上是不会变的,比如vue
、vuex
、vue-router
、element-ui
等等。这些库我们可以单独进行打包,然后通过webpack.DllPlugin
、webpack.DllReferencePlugin
和add-asset-html-webpack-plugin
引入打包后的文件。这样做的好处是:1、提高构建打包的速度,每次打包的时候可以跳过对这些公共库的打包。2、客户端缓存,因为这些公共库的版本号基本不会变化的,所以我们不需要在打包的时候给这些文件添加 hash 值。这样用户在下载了一次公共库文件之后,后续我们在进行更新,客户端还是使用的是缓存的文件。从而提高用户的体验。
DLLPlugin
是webpack
内置的插件,不需要安装。新建一个webpack.dll.config.js
文件:
const path = require("path");
const webpack = require("webpack");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
mode: "production",
entry: {
// 需要打包的公共库
vendor: ["vue", "vuex", "vue-router", "axios"],
},
output: {
// 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称
filename: "[name].dll.js",
path: path.resolve(__dirname, "lib/dll"),
// library必须和后面dllplugin中的name一致
library: "[name]_[hash]",
},
plugins: [
new CleanWebpackPlugin(),
new webpack.DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
name: "[name]_[hash]",
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(__dirname, "lib/dll", "[name]-manifest.json"),
}),
],
};
然后我们在命令行中输入
webpack --config webpack.dll.config.js
const webpack = require("webpack");
const AddAssetHtmlPlugin = require("add-asset-html-webpack-plugin");
const path = require("path");
module.exports = {
configureWebpack(config) {
const plugins = [];
plugins.push(
// 告诉webpack使用了哪些第三方库代码
new webpack.DllReferencePlugin({
// manifest.json 文件路径
manifest: require(path.resolve(
__dirname,
"lib/dll/vendor-manifest.json"
)),
})
);
plugins.push(
// 自动给html文件添加静态资源文件
new AddAssetHtmlPlugin({
// 公共库打包出来的文件路径
filepath: path.resolve(__dirname, "lib/dll/vendor.dll.js"),
// 引用路径
publicPath: "./statics/dll",
// 最终输出的目录
outputPath: "./statics/dll",
})
);
config.plugins = [...config.plugins, ...plugins];
},
};
有时候我们可能会根据一些环境变量去做不同的处理。这里我们有 2 中方式去注入全局变量,一种是借助webpack.DefinePlugin
,另外一种是借助脚手架自带的。通常我们会往全局中注入版本号,打包时间,打包人等信息,然后把这些信息挂载到 window 上面。项目放到正式环境之后可以在浏览器控制台中输入变量名称,查看版本号,打包时间等信息,方便检查是否已经进行更新了。
webpack.DefinePlugin
webpack.DefinePlugin
传入的全局变量是key-value
形式的,其中value
和key
必须是字符串。如果你想传入一个对象,就需要使用JSON.stringify
将它转化为 json 字符串。
vue.config.js
修改如下:
const webpack = require("webpack");
module.exports = {
configureWebpack(config) {
const plugins = [];
const VERSION =
require("./package.json").version.replace(/\./g, "") +
getFormatDate(new Date());
plugins.push(
new webpack.DefinePlugin({
PROJECT_MESSAGE: JSON.stringify({ version: VERSION, author: "xxx" }),
})
);
},
};
在客户端代码中访问
console.log(PROJECT_MESSAGE);
在项目根目录下放置下列文件来指定环境
.env # 在所有的环境中被载入
.env.local # 在所有的环境中被载入,但会被 git 忽略
.env.[mode] # 只在指定的模式中被载入
.env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略
其中mode
有:development、test 、production。
文件内容只能包含键=值
,如下:
FOO=bar
VUE_APP_NOT_SECRET_CODE=some_value
注意:只有NODE_ENV
、BASE_URL
和以VUE_APP_
开头的变量才能被注入到客户端代码中。
上述的方式只能是一些写死的变量,对于一些动态变量,比如版本信息,我们可以在vue.config.js
中计算环境变量,但是还是需要以VUE_APP_
开头。
const pack = require("./package.json");
process.env.VUE_APP_MESSAGE = JSON.stringify({
version: pack.version,
name: "xxx",
});
module.exports = {
// config
};
在客户端代码中访问
console.log(process.env.VUE_APP_SECRET);
在开发的时候,有些开发人员喜欢使用console.log
进行调试,但是到了部署到线上的时候,需要删除这些代码。原因如下:1、减少项目体积。别看console.log
这个代码并不多,当项目到了一定规模的时候,还是会占用不少的体积的。2、防止信息泄露。有时候我们会打印出一些用户信息,如果不及时删除,可能会泄露了用户的信息。所以我们可以在vue.config.js
中配置如下代码。
module.exports = {
configureWebpack: (config) => {
// 生产环境下生效
if (process.env.NODE_ENV === "production") {
// 配置删除 console.log
config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true;
}
},
};
对于一个 vue 项目,在打包的时候,会有一个叫chunk-vendors
的 js 文件,这个文件包含了一些业务代码和第三方库的代码,所以体积一般比较大,浏览器加载的时长也比较长。我们可以把这部分代码进行分割,分成多个文件。这样浏览器在加载时会并行加载,对于不变的代码,会直接从缓存中读取,提升了文件的访问速度。在vue.config.js
中配置如下代码:
module.exports = {
configureWebpack: (config) => {
config.when(
process.env.NODE_ENV === "production", // 配置生产环境生效
(config) => {
config.optimization.splitChunks({
chunks: "all", // 将对什么类型的代码进行分割,三种值:all: 全部 | async: 异步,按需加载的代码 | initial: 入口代码块
cacheGroups: {
// 缓存组
// 定义 libs 缓存组,分割从 node_modules 中引入的代码
libs: {
name: "chunk-libs", // 分割成的文件名
test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 中模块
priority: 10, // 优先级,当模块同时命中多个缓存组的规则时,分配到优先级高的缓存组
chunks: "initial", // 这里覆盖上面的 chunks: 'all',仅打包最初依赖的第三方库
},
// 项目使用 iview 组件开发的,定义 iviewUI 缓存组,用于分割 iview 代码
iviewUI: {
name: "chunk-iviewUI",
priority: 20, // 优先级 20,命中 iview 代码时,优先分割到此组里
test: /[\\/]node_modules[\\/]_?iview(.*)/, // 匹配 iview 代码
},
},
});
}
);
},
};
注意:在我们写 vue 路由的时候,通常会使用动态引入模块的方式来加载路由组件,方式如下:
import VueRouter from "vue-router";
const router = new VueRouter({
routes: [
{
path: "/home",
name: "home",
component: () =>
import(/* webpackChunkName: "home" */ "../views/home/index.vue"),
},
],
});
我们引入模块的时候最好可以给它来一个webpackChunkName
表示打包出来的文件名,相同功能或者模块的使用同一个webpackChunkName
表示。因为每个文件打包出来的体积可能只有几 kb,这样子会增加请求的次数,所以我们需要把相同功能或者模块的代码取一个相同的webpackChunkName
,这样子在打包的时候才能把这些代码放置到同一个文件中。反正打包出来的文件不能太大也不能太小要适中,太小会增加请求次数,太大影响加载的时长。
这种方式很少见,因为考虑到 cdn 可能会崩掉(反正我没见过崩掉),或者其他原因。不过这也是一种优化项目的方案,毕竟在自家服务器带宽或者流量有限制的时候,使用 cdn 能大大的减少后台的压力。
注意:如果采用了上面 打包分离公共库
的方案,就不要使用这种方案了。
在vue.config.js
中配置如下代码:
module.exports = {
configureWebpack: (config) => {
if (process.env.NODE_ENV === "production") {
// 配置 cdn,这里将 vue,vue-router 和 axios 三个包配置成 cdn 引入
// 其中 Vue,VueRouter 等名称是该库暴露在全局中的变量名
config.externals = {
vue: "Vue",
"vue-router": "VueRouter",
axios: "axios",
};
}
},
};
然后在public/index.html
模板文件中引入 cdn 地址:
DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.5.10/vue.min.js">script>
<script src="https://cdn.bootcdn.net/ajax/libs/vue-router/3.0.1/vue-router.min.js">script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.18.0/axios.min.js">script>
head>
<body>
<div id="app">div>
body>
html>
图片压缩会影响图片的质量的。如果对图片质量有要求的不建议使用。如果图片数量比较多,并且图片体积比较大,建议把图片进行压缩。这个需要安装image-webpack-loader
。
在vue.config.js
中配置如下代码:
module.exports = {
chainWebpack: (config) => {
// 配置图片压缩
config.module
.rule("images")
.use("image-webpack-loader")
.loader("image-webpack-loader")
.options({
bypassOnDebug: true,
})
.end();
},
};
有些依赖包,里面一些文件是没有用到的,但是打包的时候也会被一起打包进去,这个时候就可以使用IgnorePlugin
来忽略这部分文件的打包。
以 moment
为例,忽略语言包,在vue.config.js
中配置如下代码:
module.exports = {
chainWebpack: (config) => {
config.plugins.push(
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) // 忽略打包语言包
);
},
};
我们在使用 vue 脚手架创建项目的时候就已经把 eslint 的相关东西集成进来了。我们要考虑的是代码提交的时候怎么去检查代码是否符合规范,以及在提交的时候自动格式化代码,保持提交的代码统一。还有就是规范提交代码的信息。
关于这部分,我们可以借助husky
,lint-staged
这些工具。但是要注意的是,我们这里安装的husky
版本是4.x
的,最新版本已经去到了6.x
。但是6.x
版本的似乎并不是很好用,而且用法也发生了很大改变。所以我们还是用回4.x
的版本
npm i [email protected] lint-staged commitizen @commitlint/cli @commitlint/config-conventional stylelint stylelint-config-recess-order stylelint-config-recommended-scss stylelint-config-standard stylelint-scss -D
prettier
对代码进行格式化,这样可以保证提交上去的代码格式统一。然后在使用 eslint
对代码进行自动修复,最后在使用eslint
检查代码是否符合规范,符合就可以提交,不符合就不能进行提交。在package.json
中添加如下代码: "husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,vue,ts}": [
"prettier --write",
"vue-cli-service lint --fix",
"vue-cli-service lint",
"git add"
]
},
在项目根目录新建一个.prettierrc
文件,写入一下配置,用于给prettier
插件进行格式化的:
{
"singleQuote": true,
"trailingComma": "none",
"endOfLine": "auto"
}
vuecli 生成的项目只有eslint
,并没有样式文件检查的相关东西。我们需要自行进行配置。在项目根目录新建一个.stylelintrc
文件,写入以下配置,用于给stylelint
插件进行代码校验和修复的:
{
"extends": [
"stylelint-config-standard",
"stylelint-config-recommended-scss",
"stylelint-config-recess-order"
],
"plugins": [
"stylelint-scss"
],
"rules": {
"font-family-no-missing-generic-family-keyword": null,
"indentation": 2,
"no-descending-specificity": null,
"selector-pseudo-class-no-unknown": null,
"selector-pseudo-element-no-unknown": [
true, {
"ignorePseudoElements": ["v-deep"]
}
],
"property-no-unknown": null,
"declaration-colon-newline-after": null
}
}
如果你需要忽略某些样式文件,可以新建一个.stylelintignore
文件,写法跟.gitignore
文件一样
在 package.json
中添加如下代码:
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{html,vue,css,sass,scss}": [
"stylelint **/*.{html,vue,css,sass,scss} --fix"
]
},
angular
的提交信息规范。在package.json
中添加如下代码: "scripts": {
"commit": "git cz"
},
"husky": {
"hooks": {
"commit-msg": "commitlint -e $HUSKY_GIT_PARAMS"
}
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
然后再项目根目录下新建一个commitlint.config.js
文件,写入以下代码
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"build", // 主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
"ci", // 主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
"docs", // 文档更新
"examples", // 开发测试
"feat", // 新增功能
"fix", // bug 修复
"perf", // 性能优化
"refactor", // 重构代码(既没有新增功能,也没有修复 bug)
"style", // 不影响程序逻辑的代码修改(修改空白字符,补全缺失的分号等)
"test", // 新增测试用例或是更新现有测试
"revert", // 回滚某个更早之前的提交
"chore", // 不属于以上类型的其他类型(日常事务)
],
],
},
};
最后,提交代码的时候使用npm run commit
即可。
关于前端代码工作流这部分的东西,可以参考我的另一篇博客,里面有更详细的说明。
这里说明一下,我们的自动部署是针对测试环境的,并不针对生产环境的,生产环境是前端把包打好发送给后台,由后台统一去更新。
说到CICD有人可能第一时间想到的就是jenkins
。但是我们并不会去考虑使用jenkins
,原因如下
jenkins
比较吃硬件,占用内存也比较高,服务器资源有限。而且只是为了部署前端而去搞这个东西就显得有点鸡肋。jenkins
一次打包更新大概需要5-8分钟。jenkins
打包经历四部分,拉取代码–安装依赖–打包–部署。当然,慢还是跟硬件有点关系的,这就是上面所说的吃硬件。jenkins
打包更新是依靠webhook来触发的,一旦有提交就会触发webhook的。而我们提交代码都是比较细化,所以可能会提交很多次,从而多次触发更新。所以我们的做法就会自己实现一个本地自动化部署脚本,开发人员只需要运行npm run deploy
即可打包更新到测试环境。具体打包部署流程如下:拉取代码–安装依赖–打包代码–压缩成giz文件–ssh连接服务器–删除服务器上的旧文件–上传giz文件–解压giz文件–删除giz文件。这样子就很方便我们开发人员进行开发测试和更新。
关于这部分的代码,大家可以参考我的另一篇博客
另外,如果大家对jenkins
有兴趣的,可以参考我的这篇博客
上面的东西都是基于我平时工作开发中总结出来的。有的属于项目上的性能优化,比如雪碧图,图片压缩等等。有的属于工程化的,比如代码规范,自动化部署。相信这些东西大家在平常的开发中或多或少都会用到一些的。如果大家还有什么要补充的欢迎下方留言。