模仿搭建vue-cli
其实vue-cli本身就挺好用的,但是毕竟自己亲手搭的还是比较灵活,出了问题也方便解决,所以我们自己动手搭建一个脚手架
主要用到的工具
- webpack
- webpack-cli
- axios
- vue-router
package.json配置如下
{
"name": "book",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "cross-env NODE_ENV=development webpack-dev-server --history-api-fallback",
"build": "cross-env NODE_ENV=production webpack",
"build-app": "cross-env NODE_ENV=app webpack",
"start": "npm run dev"
},
"author": "",
"license": "ISC",
"devDependencies": {
"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"clean-webpack-plugin": "^2.0.1",
"config": "^3.0.1",
"copy-webpack-plugin": "^5.0.3",
"cross-env": "^5.2.0",
"css-loader": "^2.1.0",
"element-theme-chalk": "^2.5.4",
"file-loader": "^3.0.1",
"gulp": "^4.0.0",
"gulp-clean-css": "^4.0.0",
"gulp-css-wrap": "^0.1.2",
"html-webpack-plugin": "^3.2.0",
"less": "^3.9.0",
"less-loader": "^5.0.0",
"mini-css-extract-plugin": "^0.6.0",
"moment": "^2.24.0",
"node-sass": "^4.11.0",
"os": "^0.1.1",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"vue": "^2.6.6",
"vue-loader": "^15.6.2",
"vue-router": "^3.0.2",
"vue-template-compiler": "^2.6.6",
"webpack": "^4.29.3",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.1.14",
"webpack-theme-color-replacer": "^1.0.17"
},
"dependencies": {
"axios": "^0.18.0",
"element-ui": "^2.5.4",
"mint-ui": "^2.2.13",
"nprogress": "^0.2.0",
"qs": "^6.6.0",
"vue-ls": "^3.2.1",
"vuex": "^3.1.0"
}
}
webpack.config.js内容如下
const webpack = require('webpack')
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const dev_mode = process.env.NODE_ENV != 'production'
const config = require('config')
const os = require('os')
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin")
//获取ip
//注意匹配127.0.0.1的情况
let host = ''
let obj = os.networkInterfaces()
for (n in obj) {
obj[n].map(v => {
;
/^[0-9]{1,}\.[0-9]{2,}\.[0-9]{1,}\.[0-9]{1,}$/.test(v.address) &&
(host = v.address)
})
}
module.exports = {
entry: path.join(__dirname, 'src/main.js'),
output: {
filename: 'js/[name].[hash].js',
path: path.resolve(__dirname, 'dist'),
publicPath: process.env.NODE_ENV == 'app' ? "./" : "/"
},
resolve: {
alias: {
'@': path.resolve('src')
},
extensions: ['.js', '.vue', '.css', '.scss']
},
mode: (process.env.NODE_ENV == 'app' ? 'production' : process.env.NODE_ENV),
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'stage-2']
}
},
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.css$/,
use: dev_mode ? ['style-loader', 'css-loader'] : [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.less$/,
use: dev_mode ? ['style-loader', 'css-loader', 'less-loader'] : ['css-loader', 'less-loader']
},
{
test: /\.scss$/,
use: dev_mode ? ['style-loader', 'css-loader', 'sass-loader'] : [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
},
{
test: /\.(gif|jpg|jpeg|woff(2)?|ttf|eot|svg)/,
use: [{
loader: 'url-loader',
options: {
limit: 1024,
name: 'style/[name].[hash:6].[ext]'
}
}]
}
]
},
devServer: {
contentBase: path.join(__dirname, 'src'),
watchContentBase: true,
hot: true,
port: config.port,
host: '0.0.0.0',
proxy: {
'/index.php': {
target: 'http://api.guying.club',
changeOrigin: true,
secure: false
}
}
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: './index.html',
// favicon: path.resolve(__dirname, 'public/favicon.ico'),
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
}
}),
new MiniCssExtractPlugin({
filename: "style/[name].[hash].css",
chunkFilename: 'style/[id].[hash:8].css',
}),
new CleanWebpackPlugin(),
//拷贝theme.less用于换肤
new CopyPlugin([{
from: 'public',
to: ''
}])
],
performance: {
hints: false
},
optimization: {
// 公共代码抽取
// CommonsChunkPlugin 已弃用,使用optimization.splitChunks代替
// 提取被重复引入的文件,单独生成一个或多个文件,这样避免在多入口重复打包文件
splitChunks: {
cacheGroups: {
commons: {
// 选择全部chunk
chunks: "all",
// 生成的公共代码文件名,惯用vendor
name: "vendor",
// 作用于
test: /[\\/]node_modules[\\/]/
}
}
}
}
}
if (dev_mode) {
module.exports.plugins = module.exports.plugins.concat([
new webpack.HotModuleReplacementPlugin(),
new webpack.DefinePlugin({
LOCAL_ADDRESS: JSON.stringify(`http://${host}:${config.port}`)
})
])
}
MiniCssExtractPlugin 插件来抽离vue文件中的style部分为单独的css文件,放到/css下
CleanWebpackPlugin 插件在打包前删除dist文件夹
CopyPlugin 插件复制index.less到dist文件夹,因为编译以后需要用less.modifyVars实现换肤,所以需要拷贝一份样式文件
然后是自己封装的axios请求
/**axios封装
* 请求拦截、相应拦截、错误统一处理
*/
import axios from 'axios';
import QS from 'qs';
// 环境的切换
if (process.env.NODE_ENV == 'development') {
axios.defaults.baseURL = LOCAL_ADDRESS;
} else if (process.env.NODE_ENV == 'debug') {
axios.defaults.baseURL = '';
} else if (process.env.NODE_ENV == 'production') {
axios.defaults.baseURL = '';
}
// 请求超时时间
axios.defaults.timeout = 10000;
// post请求头
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
// 请求拦截器
axios.interceptors.request.use(
config => {
// 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了
// 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断
return config;
},
error => {
return Promise.error(error);
})
// 响应拦截器
axios.interceptors.response.use(
response => {
if (response.status === 200) {
return Promise.resolve(response);
} else {
return Promise.reject(response);
}
},
//服务器状态码不是200的情况
error => {
if (error.response && error.response.status) {
switch (error.response.status) {
// 401: 未登录
// 未登录则跳转登录页面,并携带当前页面的路径
// 在登录成功后返回当前页面,这一步需要在登录页操作。
case 401:
router.replace({
path: '/login',
query: { redirect: router.currentRoute.fullPath }
});
break;
// 403 token过期
// 登录过期对用户进行提示
// 清除本地token和清空vuex中token对象
// 跳转登录页面
case 403:
Toast({
message: '登录过期,请重新登录',
duration: 1000,
forbidClick: true
});
// 清除token
localStorage.removeItem('token');
store.commit('loginSuccess', null);
// 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转需要访问的页面
setTimeout(() => {
router.replace({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
});
}, 1000);
break;
// 404请求不存在
case 404:
Toast({
message: '网络请求不存在',
duration: 1500,
forbidClick: true
});
break;
// 其他错误,直接抛出错误提示
default:
Toast({
message: error.response.data.message,
duration: 1500,
forbidClick: true
});
}
return Promise.reject(error.response);
}
}
);
/**
* get方法,对应get请求
* @param {String} url [请求的url地址]
* @param {Object} params [请求时携带的参数]
*/
export function get(url, params){
return new Promise((resolve, reject) =>{
axios.get(url, {
params: params
})
.then(res => {
resolve(res.data);
})
.catch(err => {
reject(err.data)
})
});
}
/**
* post方法,对应post请求
* @param {String} url [请求的url地址]
* @param {Object} params [请求时携带的参数]
*/
export function post(url, params) {
return new Promise((resolve, reject) => {
axios.post(url, QS.stringify(params))
.then(res => {
resolve(res.data);
})
.catch(err => {
reject(err.data)
})
});
}
使用了vue-router后页面地址栏路径改变后刷新页面报错
这个主要时由于设置了vue-router为history模式,找不到路由下面的index.html页面,解决方式为配置nginx服务器的try_files,
本地解决方式为配置webpack-dev-server,增加 -history-api-fallback
使用vue-router以后地址栏改变找不到静态资源
配置webpack>output>publicPath:'/'
待续