模仿vue-cli搭建脚手架以及通用方法封装

模仿搭建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:'/'

待续

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容