灵魂拷问
有小伙伴肯定会好奇的问,这都什么年代了?还自己搭建项目模板
?你这不是瞎折腾
么?使用vue-cli
一条命令就搞定了,它不香么?
为什么要自己搭建项目模板?
从团队的角度
- 在团队中使得我们的项目结构
规范化
、统一化
,减少维护成本。 - 清晰且规范的项目结构有助于团队成员更加
快速的上手项目
,尤其是新人
。 - 可以在项目
打包
、热更新
上做到更加灵活的配置跟优化
。
从个人的角度
- 如果你想从一名
中级前端开发工程师
进阶到高级前端开发工程师
,甚至是前端架构师
,想要凸显自己的核心竞争力
,那项目脚手架的搭建
也是进阶路上不可或缺
的能力之一。 - 如果你自己不动手去撸一撸
webpack
的配置,光靠阅读一些零零散散的八股文
,怎们可能更深入的去理解各种各样的loader
、plugin
的作用?怎么可能更深入的理解项目热更新跟打包体积的优化
?
如何搭建一套webpack+vue项目模板
1. 明确项目结构
首先明确的是我们本次搭建的是单页的项目,多页我们后续再展开讨论。
万事开头难,想要搭建一个完整的项目模板,最开始也是最重要的一步,就是确定好咱们的项目结构
完整的项目结构(目前不考虑集成TS):
.
├── build
│ ├── proxy.js // 开发环境代理配置(用于跟后端联调进行代理)
│ ├── dev-server.js // 开发环境热更新配置
│ ├── webpack.config.dev.js // 开发环境配置
│ ├── webpack.config.test.js // 测试环境配置
│ ├── webpack.config.prod.js // 生产环境配置
│ ├── webpack.config.base.js // 基础配置
│ ├── plugins.js // 插件配置
├── package.json // 包管理配置
├── index.html // 入口html
├── babelrc // babel配置
├── gitignore // gitignore配置
├── eslintrc // eslintrc配置
├── README // README.md
└── src
├── api // 项目api
├── router // 页面路由
├── images // 图片
├── fonts // 字体
├── styles // 样式
├── store // 状态管理
├── config // 项目配置
├── utils // 工具类
├── components // 公共组件
│ ├── button.vue
├── main.js // 入口文件
├── page // 页面模块
│ ├── index.vue
复制代码
2. 完善项目配置
2.1 创建项目
mkdir webpack-single-template
复制代码
2.2 项目初始化
cd webpack-single-template
npm init
复制代码
一路回车即可(默认yes
),执行完成回车以后此时项目的根目录会自动帮助我们生成一个package.json
文件。
2.3 新建好项目文件目录
按照上述的项目结构依次新建好build
、src
、src/router
、src/page
、src/main.js
等目录
2.4 手动搭建webpack配置
本文的核心就是手动去配置webpack
,这里会基于目前比较成熟稳定的webpack4
来进行讲解。
在动手去配置webpack
之前,先明确我们的思路
是什么?怎么去配置?这里我把整个webpack
的工作流程比喻成工厂的流水线
,假设我们要组装生产一部智能手机:
- 流水线头:准备好需要加工的
物料
- 投放物料:由
车间工人
开始投放 - 组装加工:中间经过
螺丝工
、排线工
、主板工
、测试员
等一些列工种的组装。 - 打磨输出:组装完成之后要进行最后的打磨、输出成品
接下来我们会按照上边这个流水图来分解加工处理
我们页面组件。
一份合理的webpack
配置文件通常需要包含这六个部分:
- mode:模式,有none、development、production三个选项可以可供选择。
- none:webpack无任何预设,需要从无到有开始配置
- development:development是告诉程序,我现在是开发状态,也就是打包出来的内容要对开发友好。在此mode下,就做了以下插件的事,其他都没做,所以带’-‘号的这些插件可以省略。
// webpack.config.dev.js
module.exports = {
+ mode: 'development'
- devtool: 'eval',
- plugins: [
- new webpack.NamedModulesPlugin(),
- new webpack.NamedChunksPlugin(),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
- ]
}
复制代码
- production:通常用于生产环境,同样带’-‘号的这些插件可以省略。当设置为
production
的时候webpack
会自动帮助我们开启tree-shaking
,关于tree-shaking
我们再下一期webpack优化
中会详细讲解
// webpack.production.config.js
module.exports = {
+ mode: 'production',
- plugins: [
- new UglifyJsPlugin(/* ... */),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
- new webpack.optimize.ModuleConcatenationPlugin(),
- new webpack.NoEmitOnErrorsPlugin()
- ]
}
复制代码
2.4.1 流水线头物料箱
- entry:配置入口
我们通常在项目开发中分为:开发环境
、测试环境
、生产环境
,那么我们的webpack配置文件也是一样,配置需要围绕着这三个环境来展开。
这里先拿测试环境配置webpack.test.config.js
来测试说明
const path = require('path');
const { resolve } = path;
// 投放物料
entry: {
app: resolve(__dirname, '../src/main')
}
复制代码
- 首先安装一下
webpack
、webpack-cli
、vue
、vue-router
,记住这里我选择的是webpack4.42.0
,安装的时候需要指定一下webpack
的版本号,不然会默认安装最新的webpack5
。vue
跟vue-template-compiler
的版本号也要保持一致,不然会抛出版本不一致的异常。
// 必须安装的webpack两个插件
npm i webpack@4.42.0 webpack-cli@3.3.11 -D
// 必须安装的vue三个插件
npm i vue@2.6.11 vue-template-compiler@2.6.11 vue-router@3.1.6 vue-loader@15.9.6 -S
复制代码
- 引入
Node
中的path
模块,调用内置的resolve
函数。用于获取项目入口文件的目录的绝对路径/Users/xxx/share/webpack4/build/main
。 - 在
page
目录下新建一个index.vue(物料组件)
,router
目录中新建一个index.js(物料导航)
/page/index.vue
<template>
<div>
从0到1手把手带你捋一套webpack+vue项目模板
</div>
</template>
复制代码
/router/index.js
// 这里我们的路由采用的懒加载import的模式,可以实现组件按需加载(匹配到路由的时候才加载)
const Template = () => import(/* webpackChunkName: "index" */ '../page/index.vue');
const routes = [
{
path: '/',
component: Template,
meta: {
title: '首页'
}
}
];
export default routes;
复制代码
- 物料组件统一放在在
物料箱main.js
中集中管理。 main.js
中我们只需要引入安装好的vue-router
、并实例化router
、最后挂载
到vue实例的根节点
上即可。
main.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import routes from './router';
// 路由采用history模式
const router = new VueRouter({
mode: 'history',
routes
});
Vue.use(VueRouter);
new Vue({
router
}).$mount('#app');
复制代码
webpack
配置module
中使用vue-loader
来解析vue
文件
const VueLoaderPlugin = require('vue-loader/lib/plugin');
// 这里module跟plugin的配置规则会下边详述
module: {
rules: [
{
test: /\.vue$/,
use: ['vue-loader']
}
]
},
plugins: [
new VueLoaderPlugin()
]
复制代码
- 接下来在
package.json
中设置webpack
启动命令
"scripts": {
"test": "webpack --config build/webpack.test.config.js"
}
复制代码
这里如果善于思考的小伙伴肯定会问,为啥我配置了webpack --config
参数,就可以执行我们的webpack
文件呢?
在项目开发中,npm 在局部安装依赖后如果依赖会创建命令,那么这个依赖便会在./node_modules/.bin/ 中添加软链接,而package.json默认会访问.bin目录。所以在package.json中的script 中添加配置,可依更加便捷的启动webpack 打包 ,以及其他命令。
2.4.2 打磨输出
- output
上边我们已经配置了物料的入口,为了方便我们测试,假设我们刚开始输出的就是半成品
,这里就先把物料的出口output
也配置好。
output: {
filename: 'js/[name]-[chunkhash:6].js',
path: resolve(__dirname, '../dist')
}
复制代码
filename
:用于指定我们打包后输出的js文件名称,为了记录每次编译文件的版本,我们会把输出的文件通过三种hash模式
加上文件指纹。
关于文件指纹,在下边的loader
配置中我们会陆续使用到,这里先在出口output
中配置Chunkhash
// 设置 file-loader 的 name,使用 [hash],用于图片、字体等
Hash:和整个项⽬目的构建相关,只要项⽬目⽂文件有修改,整个项⽬目构建的 hash 值就会更更改
// 使用场景:设置 output 的 filename
Chunkhash:和 webpack 打包的 chunk 有关,不不同的 entry 会⽣生成不不同的 chunkhash 值
// 使用在提取css插件 MiniCssExtractPlugin 的 filename
Contenthash:根据⽂文件内容来定义 hash ,⽂文件内容不不变,则 contenthash 不不变
复制代码
path
:指定打包后的目录,这里跟入口配置差不多,我们配置成'../dist'
,执行打包命令以后就会在项目的根目录下自动帮助我们生成一个dist目录
。
- 执行
npm run test
测试
npm run test
复制代码
这样下来,我们的第一个半成品手机
就加工出来了,即打包输出的文件dist
。为了每次打包出来的js文件
都是最新的,所以我们在每次打包之前都需要将老的dist
目录先删除掉,这里我选择使用rimraf
插件,当然webpack
中也提供了clean-webpack-plugin
插件,功能都是相同的,二者可以取其一即可。
package.json
中的script
优化后:
"scripts": {
"test": "rimraf dist && webpack --config build/webpack.test.config.js"
}
复制代码
然后我们要想生产一部功能完整的智能手机,上述的步骤还远远达不到,我们还需要经历各个各样的工种(loader、plugin
)来一起参与加工才可以。
2.4.3 组装加工:排线工
- module
- 这里我把
module
称作大对象,大对象中传入的是rules
数组,数组中又传入的不同的小对象,每一个loader
分别对应着不同的小对象。 - 每一个小对象又分别传入两个基础参数:
test
跟use
,test
这里使用正则表达式来匹配我们想要转译文件的规则。use
配置我们具体使用什么loader
- 上边我们为了解析vue的语法,我们已经配置了
vue-loader
,那我们在组件中需要对页面进行装饰用到了less
,就需要less-loader
、css-loader
、style-loader
、postcss-loader
;
先安装一下对应的插件跟loader:
为了避免之前说过的包版本依赖关系出现的问题,所以这里我都指定了具体的安装包的版本号。
npm i less@3.11.1 less-loader@5.0.0 css-loader@3.4.2 style-loader@1.1.3 postcss-loader@3.0.0 autoprefixer@9.7.4
复制代码
// less-loader:把less编译成css
// css-loader:可以让 webpack 解析 css(因为 webpack 原生只支持 js 和 json 的解析),并且将解析出来的 css 转换成一个对象,插入到 JS 里面去
// style-loader:向 DOM 插入 style 标签,并且将样式插入进去,这样网页才能解析到
// postcss-loader和autoprefixer:为了兼容不同浏览器的样式的解析,我们可以借助这两个插件,在打包的时候自动帮助我们补齐css的浏览器兼容内核前缀
// 执行的顺序是从右到左
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer')({
// 兼容最新的两个浏览器版本
overrideBrowserslist: ['last 2 version', '>1%', 'ios 7']
})
]
}
},
]
}
复制代码
index.vue
<style lang="less" scoped>
div {
display: flex;
flex-direction: column;
justify-content: center;
color: red;
font-size: 20px;
}
</style>
复制代码
此时我们再次执行npm run test
,去dist
目录下打包好的js
文件中会发现,咱们写的less
也被编译成了css
,而且每行样式前默认加上了兼容的浏览器前缀。
- 由于浏览器是无法识别我们在组件中使用的
es6
的语法,所以我们还需要借助babel-loader
把es6
编译成浏览器可以识别的es5
先安装一下babel
相关的包
npm i babel-loader@8.0.6 @babel/core@7.8.7
复制代码
{
test: /\.js$/,
use: ['babel-loader'],
// 为了减少每次编译搜索的范围,我们把node_modules中的依赖排除掉
exclude: /node_modules/
}
复制代码
index.vue
<script>
export default {
data() {
return {
msg: '从0到1手把手带你捋一套webpack+vue项目模板'
}
},
mounted() {
this.babelCompileTest();
},
methods: {
async babelCompileTest() {
setTimeout(() => {
console.log("babelCompileTest");
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log('first', first);
console.log('rest', rest);
}, 10);
}
}
}
</script>
复制代码
url-loader
跟file-loader
上边我们已经搞定了css
、js
的编译,那接下来我们就来处理图片跟字体
先安装一下相关的包
npm i file-loader@5.1.0 url-loader@3.0.0
复制代码
// 图片这里我采用的是url-loader来编译
// 字体这里我采用的是file-loader来编译
{
test: /\.(jpg|jpeg|png|gif)$/,
loaders: 'url-loader',
exclude: /node_modules/,
// 这里设置了limit参数,为了减少图片资源的加载次数,将小于8kb的图片走base64方式去加载
options: {
limit: 8192,
// 输出的目录
outputPath: 'img/',
// 同时使用的是hash模式给打包后的图片加上前缀
name: '[name]-[hash:6].[ext]'
}
},
{
test: /\.(woff|woff2|svg|eot|ttf)$/,
use: [
{
loader: 'file-loader',
options: {
// 输出的目录
outputPath: 'fonts/',
name: '[name].[ext]'
}
}
]
}
复制代码
index.vue
<template>
<div>
{{ msg }}
<img :src="https://juejin.cn/post/require('../images/1.jpeg')">
</div>
</template>
复制代码
2.4.4 组装加工:主板工
接下来就要轮到很重要的角色主板工了,一部手机的制作核心其实就是主板这块,那webpack
打包编译也是这样,在这里plugin
我比作就是主板工
,它的工作流程贯穿这整个webpack
的生命周期,很多loader
没法做的事情,都可以借助plugin
来实现。
- plugins
为了方便plugin
的管理,我们将上边webpack.test.config.js
中的plugins
单独抽离出来到build/plugins.js
中进行维护。
plugins.js
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const PluginConfig = [
new VueLoaderPlugin()
];
module.exports = PluginConfig;
复制代码
上边我们只是把测试环境
的配置讲解完了,那接下来我们就要配置开发环境
跟生产环境
了,不管是开发环境、测试环境、还是生产环境
,entery
、output
、module
、plugin
都需要配置,那这样的话如果我们在webpack.dev.config.js
、webpack.test.config.js
、webpack.prod.config.js
中每个都配上一遍的话,就带来很多重复的工作量,而且也不方便后续我们维护,所以这里我们把这些公共的配置统一抽离到webpack.config.base.js
中。
webpack.config.base.js
const Path = require('path');
const BasePlugins = require('./plugins');
const { resolve } = Path;
module.exports = {
entry: {
app: resolve(__dirname, '../src/main')
},
output: {
filename: 'js/[name]-[chunkhash:6].js',
path: resolve(__dirname, '../dist')
},
module: {
rules: [
{
test: /\.vue$/,
use: ['vue-loader']
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer')({
overrideBrowserslist: ['last 2 version', '>1%', 'ios 7']
})
]
}
},
]
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer')({
overrideBrowserslist: ['last 2 version', '>1%', 'ios 7']
})
]
}
},
]
},
{
test: /\.js$/,
use: ['babel-loader'],
exclude: /node_modules/
},
{
test: /\.(jpg|jpeg|png|gif)$/,
loaders: 'url-loader',
exclude: /node_modules/,
options: {
limit: 8192,
outputPath: 'img/',
name: '[name]-[hash:6].[ext]'
}
},
{
test: /\.(woff|woff2|svg|eot|ttf)$/,
use: [
{
loader: 'file-loader',
options: {
outputPath: 'fonts/',
name: '[name].[ext]'
}
}
]
},
]
},
plugins: [
...BasePlugins
]
}
复制代码
webpack.config.dev.js
开发环境下只需要通过webpack-merge
插件把webpack.config.base.js
合并进去即可。
安装webpack-merge
npm i webpack-merge@4.2.2
复制代码
开发环境下除了我们的mode
要设置成development
之外,最重要的就是热更新
了,关于热更新
,webpack
提供了两个可选的插件可以使用,分别是webpack-dev-server
、以及webpack-dev-middleware
&webpack-hot-middleware
。为了更灵活的配置前后端联调代理,这里我没有采用官方推荐的webpack-dev-server
,而是采用第二种方案在基于express
基础上配合webpack-dev-middleware
&webpack-hot-middleware
来实现热更新。
先安装一下热更新需要的依赖包
npm i express@4.17.1 http-proxy-middleware@0.19.1 webpack-dashboard@3.0.7 webpack-dev-middleware@3.7.0 webpack-hot-middleware@2.25.0 connect-history-api-fallback@1.6.0 opn@6.0.0
复制代码
dev-server.js
const opn = require('opn');
const Express = require('express');
const Webpack = require('webpack');
const ProxyMiddleware = require('http-proxy-middleware');
const DashboardPlugin = require('webpack-dashboard/plugin');
const WebpackDevMiddleware = require('webpack-dev-middleware');
const WebpackHotMiddleware = require('webpack-hot-middleware');
const HistoryApiFallback = require('connect-history-api-fallback');
const WebpackConfig = require('./webpack.config.dev');
const Config = require('./proxy');
// 端口
const port = process.env.PORT || Config.port;
// 是否需要自动打开页面
const autoOpenBrowser = !!Config.autoOpenBrowser;
// 代理配置
const { proxyTable } = Config;
// 开启express服务
const app = Express();
const compiler = Webpack(WebpackConfig);
// 编译
const devMiddleware = WebpackDevMiddleware(compiler, {
stats: {
colors: true,
chunks: false
}
});
// 热更新
const hotMiddleware = WebpackHotMiddleware(compiler, {
log: () => {}
});
// 触发页面刷新
compiler.plugin('compilation', compilation => {
compilation.plugin('html-webpack-plugin-after-emit', () => {
hotMiddleware.publish({ action: 'reload' });
});
});
// 终端编译信息输出
compiler.apply(new DashboardPlugin());
// 代理
Object.keys(proxyTable).forEach(context => {
let options = proxyTable[context];
if (typeof options === 'string') {
options = { target: options };
}
app.use(ProxyMiddleware(options.filter || context, options));
});
// 设置允许跨域
app.all('*', (req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
next();
});
// 开启history路由模式
app.use(HistoryApiFallback());
app.use(devMiddleware);
app.use(hotMiddleware);
const uri = `${Config.protocol}://${Config.host}:${port}`;
// 打印启动日志
devMiddleware.waitUntilValid(() => {
console.log(`> Listening at ${uri}\n`);
});
// 监听服务
app.listen(port, err => {
if (err) {
console.log(err);
return;
}
// 自动打开页面
if (autoOpenBrowser) {
opn(uri);
}
});
复制代码
build
目录下新建proxy.js
主要用于配置前后端联调的代理
module.exports = {
// 本地访问host,可自行配置
host: 'localhost',
// 本地访问协议
protocol: 'http',
// 监听端口
port: 8000,
// 设置启动成功后默认打开浏览器
autoOpenBrowser: true,
assetsSubDirectory: '',
assetsPublicPath: '',
// 此处用于很后端联调的代理配置
proxyTable: {
'/api': {
target: 'http://www.baidu.com',
pathRewrite: {
'^/api': '/'
},
changeOrigin: true
}
},
}
复制代码
然后,我们还需要安装一下html-webpack-plugin
这个插件,根据模板html文件每次编译后自动帮助我们生成新的html文件,并将样式link
插入到head
元素中,script
插入到head
或者body
中。
npm i html-webpack-plugin@3.2.0 -D
复制代码
plugins.js
new HtmlWebpackPlugin({
// 打包输出的index.html
filename: '../dist/index.html',
// 项目跟目录新建的index.html
template: resolve(__dirname, '../index.html')
}),
复制代码
项目的根目录下新建模板index.html
文件作为我们的项目启动入口文件
<!DOCTYPE html>
<html lang="en">
<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">
<title>从0到1手把手带你捋一套webpack+vue项目模板</title>
</head>
<body>
<div id="app">
<!-- 路由初始化,这里必须配置 -->
<router-view></router-view>
</div>
</body>
</html>
复制代码
package.json
中的script
新增一条开发环境启动命令:
"scripts": {
// 因为我们是本地通过express其的服务,所以直接使用node命令来启动dev-server.js即可
"dev": "node build/dev-server.js"
}
复制代码
运行
npm run dev
复制代码
开发环境我们就启动成功了,尝试在index.vue
中去修改,热更新
也是正常的。
webpack.config.test.js
测试环境下我们只需要把mode
设置为production
,devtool
设置为source-map
即可,关于source-map
,下边会详细讲解。
const Path = require('path');
const WebpackMerge = require('webpack-merge');
const WebpackConfig = require('./webpack.base.config.js');
const { resolve } = Path;
module.exports = WebpackMerge(WebpackConfig, {
mode: 'production',
devtool: 'source-map',
entry: {
app: resolve(__dirname, '../src/main')
}
})
复制代码
webpack.config.prod.js
生产环境下我们只需要把Mode
设置为production
,跟测试环境唯一的区别是devtool
设置为hidden-source-map
,避免生产环境我们把map文件直接暴露。
const Path = require('path');
const WebpackMerge = require('webpack-merge');
const WebpackConfig = require('./webpack.base.config.js');
const { resolve } = Path;
module.exports = WebpackMerge(WebpackConfig, {
mode: 'production',
devtool: 'hidden-source-map',
entry: {
app: resolve(__dirname, '../src/main')
},
plugins: [
// 为了方便排查我们在生产环境打包是不是最新的,每次构建自动添加构建时间戳
new webpack.BannerPlugin(new Date().toString()),
]
})
复制代码
2.4.5 优化解析
- resolve
为了每次编译打包减少webpack
搜索范围,我们可以在resolve
中添加extensions
跟alias
webpack.config.base.js
resolve: {
// 解析文件范围
extensions: ['.js', '.json', '.css', '.less', '.vue'],
// 设置别名
alias: {
vue$: 'vue/dist/vue.common.js',
'@': resolve(__dirname, '../src')
}
}
复制代码
- devtool:是否开启
sourcemap
,以及不同场景对应着不同的sourcemap
配置
配置之前我们先普及一下sourcemap
:
source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。
sourcemap 的原理
- 当源码
parse
成AST
的时候,会在AST
中保留它在源码中的位置(line、column
) AST
进行转换并不会修改这个行列号
生成目标代码的时候,又会计算出一个新的位置(line、column
)
- 这样两个位置合并起来就是一个
mapping
。所有AST
节点的mapping
就能生成完整的sourcemap
。
调试代码时定位到源码
chrome、firefox 等浏览器支持在文件末尾加上一行注释
//# sourceMappingURL=http://example.com/path/to/your/sourcemap.map
复制代码
可以通过 url
的方式或者转成 base64
内联的方式来关联 sourcemap
。浏览器会自动解析 sourcemap
,关联到源码。这样打断点、错误堆栈等都会对应到相应源码。
在开发环境跟测试环境中,为了方便我们断点调试
、排查bug
,我们就设置为'source-map'
,生产环境下是万万不能把sourcemap
开启的,我们可以借助sentry-webpack-plugin
插件把sourcemap
文件上传到sentry
后台,这就就可以很好的在sentry
后台去定位错误代码的具体位置了。
// 开发环境&测试环境
devtool: 'source-map'
// 生产环境
devtool: 'hidden-source-map'
复制代码
好了,经过上述从流水线头投放物料
再到最后打磨输出
,我们一部完整的智能手机
就生产出来了,对应着我们整个webpack
从开发环境热更新
再到测试环境、生产环境打包
的完整流程就讲解完了,那要想我们的项目结构达到完善,上边这些是远远不够的,接下来我们再对项目做出进一步的完善。
3. 项目完善
3.1 公共样式
我们可以把系统中公共的样式部分抽离到style/common.less
下,便于维护,我这里只是简单的罗列一些公共的样式,具体需要根据自己的需求往里添加维护即可。
style/common.less
* {
margin: 0;
padding: 0;
}
html {
color: #303133;
font-size: 14px;
font-family: "Microsoft YaHei", "微软雅黑";
}
ul {
list-style: none;
}
li {
list-style: none;
}
.fl {
float: left;
}
.fr {
float: right;
}
.clearfix {
zoom: 1;
&::after {
content: '';
height: 0;
line-height: 0;
display: block;
visibility: hidden;
clear: both;
}
}
复制代码
3.2 store
主要用于存放我们vuex
中的数据,当一个项目变的庞大起来,必然会有一些公共的值需要我们集中存储管理,这样才可以方便我们在系统中随时随地使用,并且这些值还可以做到响应式
。
关于vuex
的实现原理,我这里就不详细讲解了,可以直接看vuex的官方文档,这里主要说一下vuex
是如何使用的。
这里我模拟一个简单版本的用户信息状态存取,store
目录分为以下这几个文件:
首先安装一下vuex
npm i vuex@3.1.1 -S
复制代码
- action.js
dispatch一个action
import { queryUserInfo } from '@/api';
import { USER_INFO } from './mutation-type';
const fetchUserInfo = ({ commit }) => (
new Promise((resolve, reject) => {
queryUserInfo().then(res => {
if (res) {
commit(USER_INFO, res.data);
} else {
commit(USER_INFO, {});
}
resolve(res);
}).catch(e => reject(e));
})
);
export default {
fetchUserInfo
}
复制代码
- getters.js
获取getter
export default {
userInfo: state => state.userInfo
}
复制代码
- index.js
vuex实例化
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import mutations from './mutations';
import actions from './actions';
import getters from './getters';
Vue.use(Vuex);
export default new Vuex.Store({
state,
mutations,
actions,
getters
});
复制代码
- mutation-type.js
export const USER_INFO = 'USER_INFO';
复制代码
- mutations.js
import { USER_INFO } from './mutation-type';
export default {
[USER_INFO](state, data) {
state.userInfo = data;
}
}
复制代码
- state.js
参数初始化
export default {
userInfo: {}
}
复制代码
index.vue
<script>
import { mapGetters, mapActions } from 'vuex';
export default {
data() {
return {
msg: '从0到1手把手带你捋一套webpack+vue单页项目模板'
}
},
computed: {
...mapGetters({
userInfo: 'userInfo'
})
},
mounted() {
this.babelCompileTest();
this.fetchUserInfo();
},
methods: {
...mapActions(['fetchUserInfo']),
babelCompileTest() {
setTimeout(() => {
console.log('babelCompileTest');
}, 10);
}
}
}
</script>
复制代码
3.3 fetch
一个完善的系统,fetch
请求的封装是必不可少的,我们可以在axios
的基础上做一层简单的封装。
先安装一下依赖
npm i axios@0.18.0 -D
复制代码
import Axios from 'axios';
import Vue from 'vue';
const vm = new Vue();
const fetch = async (config) => {
// 基础参数
const {
url, method, params, timeout, ...other
} = config;
// 创建axios实例并配置默认值
const axiosIns = Axios.create({
// 请求头可以按需配置
headers: {},
// 设置响应超时时间
timeout: timeout || 1 * 60 * 1000
});
// 处理请求参数
let options;
if (method === 'post') {
options = {
url,
method: 'post',
data: params,
...other
};
} else {
options = {
url,
method: 'get',
params,
...other
};
}
// 处理响应结果
const response = await axiosIns(options).then((res) => {
let { code } = res.data;
const { msg } = res.data;
if (code !== '0') {
vm.$message.error(msg);
return false;
}
return res.data;
}).catch((error) => {
console.log(error)
return false;
});
return response;
};
export default fetch;
复制代码
api/index.js
import Fetch from '@/utils/fetch';
export const queryUserInfo = params => (
Fetch({
url: '/api/query_user_info',
params
})
);
复制代码
3.4 eslint + pre-commit
eslint
一个完善的项目,eslint
同样也是必不可少的,不管是多人开发
,还是单人开发
,我们都有必要制定一些开发规范来保证我们的代码高质量
、高输出
,具有可维护性
。
先来安装一下eslint
相关的依赖
npm i eslint@5.16.0 babel-eslint@10.0.1 eslint-plugin-vue@5.2.2 -S
复制代码
在项目的跟目录新建一个.eslintrc.js
文件,这里只是简单的罗列一下跟vue、js
相关的eslint
规则,详细的配置可以直接参考eslint官网。
module.exports = {
"root": true,
"extends": [
// 使用 eslint-plugin-vue 插件,并继承 eslint-config-vue 的 recommended 配置
"plugin:vue/recommended"
],
// 自定义 parser,详见 https://eslint.vuejs.org/user-guide/#how-to-use-custom-parser
"parserOptions": {
"parser": "babel-eslint"
},
rules: {
"semi": ["error", "always"],
"quotes": ["error", "double"]
}
};
复制代码
package.json
中script
添加一条校验eslit
命令,用于对vue
、js
文件等检查。
"lint": "eslint --fix --ext .vue,.js src build"
复制代码
pre-commit
光有eslint
的校验还不行,有的小伙伴还是可以通过--no-verify
去直接忽略eslit
的校验,为了让我们的eslit
校验更有价值,我们可以在每次本地git commit
之前对项目中的eslit
进行校验,如果有eslint
错误没有修改,阻止commit
,修复后才可以提交。这里我们可以使用pre-commit
来帮助我们做这件事。
先来安装一下pre-commit
相关的依赖
npm i pre-commit@1.2.2 lint-staged@8.1.7 -D
复制代码
package.json
中新增lint-staged
跟pre-commit
配置
"pre-commit": [
"lint-staged"
],
"lint-staged": {
"*.{js, vue}": [
"eslint --fix",
"git add"
]
}
复制代码
3.5 premiter
Prettier 简介
Prettier 是由 Facebook 公司开发的opinionated的代码格式化工具,它移除了代码的原始风格,并确保所有输出的代码遵守一致的风格。
为什么使用 Prettier
- 构建和强制一套风格指南
- 帮助新人
- 帮助开发者自动格式化代码,节省大量时间
如何使用Prettier
我们首选在VS Code
中进行配置:
安装VS Code
的 ESLint
插件和 Prettier
– Code formatter
插件,并在 Code -> 首选项(Preferences) -> 设置(Settings)
,添加如下配置:
{
// Format a file on save. A formatter must be available, the file must not be auto-saved, and editor must not be shutting down.
"editor.formatOnSave": true,
// 设置文件默认的格式化工具为 Prettier - Code formatter
// 当文件存在多个 VS Code 的 formatter 插件对同一个文件类型进行格式化时,需要手动选择 prettier-vscode 即 Prettier - Code formatter 插件
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
// 关闭编辑器对 js 文件的格式化,交给 ESLint 来做格式化,否则会格式化两次
"editor.formatOnSave": false
},
// Eslint 插件配置,详见 https://github.com/microsoft/vscode-eslint
// Enables auto fix on save. Please note auto fix on save is only available if VS Code's files.autoSave is either off, onFocusChange or onWindowChange. It will not work with afterDelay.
"eslint.autoFixOnSave": true,
"eslint.alwaysShowStatus": true,
// An array of language ids which should be validated by ESLint
"eslint.validate": [
{
"language": "html",
"autoFix": true
},
{
"language": "javascript",
"autoFix": true
},
{
"language": "vue",
"autoFix": true
}
],
// Vetur(若安装了此插件的话)
// 关闭 vetur 的格式化功能
"vetur.format.enable": false,
// 关闭 vetur 对 template 的检查,交给 eslint,详见:https://vuejs.github.io/vetur/linting-error.html#linting-for-template
"vetur.validation.template": false
}
复制代码
3.6 README(文档)
一个完善的项目离不开好的文档说明。
见根目录README.md
3.7 commit规范
一个项目落地往往是多人在一起开发,我们除了要定制eslint
、Prettier
规范外,同时还需要指定git commit
规范。好处就是让我们每一次提交都可以清楚的知道你当前的提交是优化
、还是改bug
、或者是合并分支
等,可以让我们的commit日志更加语义化
,同时也方便我们更加友好的查看提交日志。
常见的type类型有:
- feat:新增功能
- build:主要目的是修改项目构建系统(例如 glup,webpack的配置等)的提交
- fix:bug 修复
- merge:分支合并
- upd:更新某功能
- test:新增测试用例或是更新现有测试
- ci:主要目的是修改项目继续集成流程
- docs:文档更新
- style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
- revert:回滚某个更早之前的提交
eg:
git commit -m"feat:新增弹窗组件"
git commit -m"fix:修改了弹窗组件点击确定按钮无法发送请求的问题"
复制代码
4. 项目优化
关于项目优化,主要针对webpack优化
,会在下一篇文章中具体讲解。
总结
这里是项目的源码,欢迎Star
或Fork
。
- 利用五一假期完成了一个相对完善的
webpack + vue
的单页项目模板。有些地方写的可能有些粗糙,欢迎小伙伴们在评论区留言指正,我们一起成长。 - 下篇文章会针对复杂项目的
打包体积
跟热更新
上做出具体的优化与实操方案讲解。
如果您觉得这篇文章对您有一点点帮助,欢迎看完后给我点赞,您的点赞是我前进的动力,谢谢~~