webpack搭建vue3项目

本文只讲不使用vue-cli徒手搭建vue3项目,目的是了解日常开发常用模块搭配,了解各个模块的功能作用,以便自己可以搭建除vue之外的(如rect或原生开发的)项目。实际开发时,如果使用到vuerect,建议使用官方cli创建项目,然后再安装其他常用模块会方便很多。

目标

  1. 打包压缩
  2. 热更新
  3. 编译ES6+使兼容主流浏览器
  4. 安装vue
  5. 支持编译scss
  6. css分离打包
  7. 固定模块单独打包
  8. css3兼容处理
  9. 响应式单位处理
  10. 静态资源处理
  11. 接口代理
  12. 多页面开发
一、实现打包压缩
  1. 运行npm init -y,此时在目录下生成了package.json文件
  2. 运行npm install -D webpack webpack-cli安装webpack,版本如下:
"webpack": "^5.39.0",
"webpack-cli": "^4.7.2"
  1. 在项目目录下创建目录src,并在src内创建index.js文件
demo
|-- node_modules
|-- src
  |-- index.js
|-- package.json
  1. index.js随便写一些代码
async function fn(){
    let n = await new Promise((r => {
        setTimeout(() => {
            r(1)
        }, 1000);
    }))
    return n
}
fn().then(n => {
    console.log(n)
})
  1. package.json文件内添加devbuild指令:
"scripts": {
    "dev": "webpack --mode development",
    "build": "webpack --mode production"
}

此时,运行npm run devnpm run build即可正常打包,如运行npm run build,会生成打包压缩后的文件dist/main.js,其代码如下:

(async function(){return await new Promise((e=>{setTimeout((()=>{e(1)}),1e3)}))})().then((e=>{console.log(e)}));

可以看到代码已被打包压缩,因webpack4+开始,内部使用了terser压缩工具(据说uglify-js不支持ES6+)。

PS:可以通过配置webpack.config.js如下关闭压缩

module.exports = {
  optimization: false
}

当然,大多数时候这是不必要的,但也会有它的使用场景,比如使用webpack开发小程序,由于development模式webpack会使用到eval,而小程序不支持eval,而开发时又不希望编译太慢(压缩极影响编译速度),此时就可以使用production模式然后关掉optimization


二、实现热更新
  1. 运行npm i -D webpack-dev-server html-webpack-plugin,版本如下:
"html-webpack-plugin": "^5.3.1",
"webpack-dev-server": "^3.11.2"
  1. package.json添加start指令:
"scripts": {
    "start": "webpack serve --mode development --open",
    "dev": "webpack --mode development",
    "build": "webpack --mode production"
}
  1. 在项目目录下添加webpack.config.js文件,代码如下:
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    target: 'web',
    devServer: {
        contentBase: './dist',
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Demo'
        }),
    ]
}

此时,运行npm start,自动打开浏览器并打开地址http://localhost:8080

三、实现编译ES6+

从上方打包后的代码看到,并没有编译ES6+为ES5,所以需要使用babel来编译。

  1. 运行npm i -D @babel/core @babel/preset-env babel-loader,版本如下:
"@babel/core": "^7.14.6",
"@babel/preset-env": "^7.14.5",
"babel-loader": "^8.2.2",
  1. webpack.config.js中添加
module: {
  ...
  rules: [{
      test: /\.m?js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
          loader: 'babel-loader',
          options: {
              presets: ['@babel/preset-env']
          }
      }
  }],
  ...
}

此时,运行npm run build后,打包的代码虽然把let const async/await等语法转换了,但还不完全是ES5代码,比如Promise对象。所以,接下来就来解决polyfill的问题。

  1. 运行npm i --save core-js,它就是把polyfill拆分成小颗粒的包代码库,以便babel-loader动态的import使用到的对象。版本如下:
"core-js": "^3.14.0"
  1. package.json添加字段browserslist关于browserslist
"browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead",
    "Android >= 4.4",
    "iOS >=5"
]

这是移动端兼容目标浏览器的常用配置,不考虑IE浏览器。

  1. webpack.config.js中修改babel-loader的配置如下:
{
    test: /\.m?js$/,
    exclude: /(node_modules|bower_components)/,
    use: {
        loader: 'babel-loader',
        options: {
            // 编译必须排除core-js中的代码,不然可能会发生错误
            exclude: [
                /node_modules[\\\/]core-js/,
                /node_modules[\\\/]webpack[\\\/]buildin/,
            ],
            presets: [['@babel/preset-env', {
                useBuiltIns: 'usage',
                corejs: 3
            }]]
        }
    }
}

此时运行npm run build就可以看到控制台动态的打包了兼容处理的代码库

PS: 如果觉得动态import不靠谱,那么也可以选择简单粗暴直接安装babel-polyfill,然后直接import 'babel-polyfill'

  1. 运行npm i --save whatwg-fetch安装whatwg-fetch,这是对web端的fetch的兼容处理,使能够在不同浏览器使用fetch发送异步请求。这种单颗粒的直接在文件开头import 'whatwg-fetch'即可。
四、安装vue 3.x
  1. 运行npm i --save vue@next安装vue
  2. 运行npm i -D vue-loader@next @vue/compiler-sfc,让webpack支持单文件组件(sfc)
  3. 版本号如下:
"@vue/compiler-sfc": "^3.1.1",
"vue-loader": "^16.2.0",

"vue": "^3.1.1",
  1. webpack.config.js配置VueLoaderPluginvue-loader。另外,规范一下文件名和目录,把src目录下的index.js文件更名为main.js,在项目目录下新增目录public并添加index.html文件,修改wepback.config.js的对应配置。

目前为止,webpack.config.js完整配置如下:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');

module.exports = {
    target: 'web',
    entry: {
        app: './src/main.js'
    },
    devServer: {
        contentBase: './dist',
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Demo',
            template: './public/index.html'
        }),
        new VueLoaderPlugin()
    ],
    module: {
        rules: [{
            test: /\.m?js$/,
            exclude: /(node_modules|bower_components)/,
            use: {
                loader: 'babel-loader',
                options: {
                    exclude: [
                        /node_modules[\\\/]core-js/,
                        /node_modules[\\\/]webpack[\\\/]buildin/,
                    ],
                    presets: [['@babel/preset-env', {
                        useBuiltIns: 'usage',
                        corejs: 3
                    }]]
                }
            }
        },{
            test: /\.vue$/,
            loader: 'vue-loader'
        }]
    }
}

public/index.html文件代码如下:

<!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">
    <title>Demo</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>
  1. src目录创建App.vue文件,代码如下:
<template>
  <div>Hello Webpack!</div>
</template>

<script>
export default {
    data(){}
}
</script>

<style>
</style>
  1. src/main.js的代码修改如下:
import {createApp} from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

到此,vue3基本开发支持已正常完成,可以运行npm start看看。

五、使用支持编译scss
  1. 运行npm i -D style-loader css-loader sass-loader sass,并在webpack.config.jsmodule.rules字段添加如下配置:
},{
    test: /\.s[ac]ss$/i,
    use: ['style-loader', 'css-loader', 'sass-loader']
}]
  1. src/App.vue文件修改style标签并写简单的样式如下
<style lang="scss">
#app{
    div{
        color: red
    }
}
</style>

运行npm start不出意外的话,应该看到文字变红了。

六、分离css

目前配置运行npm run build打包,jscss是完全揉在一起打包进app.js文件的,我们希望把css代码从app.js中分离为单独的css文件。

  1. 运行npm i -D mini-css-extract-plugin安装插件,版本如下:
"mini-css-extract-plugin": "^1.6.0",
  1. 修改webpack.config.js,由于在development模式下,为了快速编译,故不应该分离css,而应该只在production模式使用这个插件。顺便,对不同开发模式做不同的其他配置项修改。完整配置如:
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const isProd = process.env.NODE_ENV === 'production'
const prodPlugins = []
if(isProd){
    prodPlugins.push(new MiniCssExtractPlugin({
        filename: '[name].[contenthash].css'
    }))
}
module.exports = {
    target: 'web',
    mode: process.env.NODE_ENV,
    entry: {
        app: './src/main.js'
    },
    output: {
        clean: isProd,
        filename: isProd ? '[name].[contenthash].js' : '[name].js' 
    },
    devServer: {
        contentBase: './dist',
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Demo',
            template: './public/index.html'
        }),
        new VueLoaderPlugin(),
        ...prodPlugins
    ],
    module: {
        rules: [{
            test: /\.m?js$/,
            exclude: /(node_modules|bower_components)/,
            use: {
                loader: 'babel-loader',
                options: {
                    exclude: [
                        /node_modules[\\\/]core-js/,
                        /node_modules[\\\/]webpack[\\\/]buildin/,
                    ],
                    presets: [['@babel/preset-env', {
                        useBuiltIns: 'usage',
                        corejs: 3
                    }]]
                }
            }
        },{
            test: /\.vue$/,
            loader: 'vue-loader'
        },{
            test: /\.s[ac]ss$/i,
            use: [isProd ? MiniCssExtractPlugin.loader : 'style-loader', 'css-loader', 'sass-loader']
          },
        ]
    }
}

上方代码中,process.env.NODE_ENV目前是undefined的,我们需要修改package.json的指令,在执行环境中加入NODE_ENV变量。这里需要安装cross-env来处理跨平台兼容。

  1. 运行npm i -D cross-env,安装版本如下:
"cross-env": "^7.0.3",
  1. 修改package.json指令如下:
"scripts": {
    "start": "cross-env NODE_ENV=development webpack serve --open",
    "dev": "cross-env NODE_ENV=development webpack",
    "build": "cross-env NODE_ENV=production webpack"
},

这样,当运行npm run build时,就可以看到css被分离了。

七、固定模块单独打包

因为像vue的源码部分是固定不变的,应该把它与页面的逻辑代码分离出去,这样在项目版本跌代时,vue就可以来自缓存,而只需要加载逻辑代码。

  1. webpack.config.js添加如下配置:
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendor',
                    chunks: 'all',
                },
            },
        },
    },

意思是把所有的来自node_modules模块都打包到vendor.js中。

八、css3兼容处理
  1. 运行npm i -D postcss-loader postcss postcss-preset-env,版本如下:
"postcss": "^8.3.5",
"postcss-loader": "^6.1.0",
"postcss-preset-env": "^6.7.0",
  1. 修改webpack.config.js中的module.rules里的css-loader后加入如下:
        {
            test: /\.s[ac]ss$/i,
            use: [
                isProd ? MiniCssExtractPlugin.loader : 'style-loader',
                'css-loader',
                {
                    loader: 'postcss-loader',
                    options: {
                        postcssOptions: {
                            plugins: [
                                'postcss-preset-env'
                            ],
                        },
                    },
                },
                'sass-loader'
            ]
        }
  1. App.vue中添加css3代码,然后运行npm start测试看看
<style lang="scss">
#app{
    div{
        color: purple;
        display: inline-block;
        animation: rotating 6s linear infinite;
    }
}
@keyframes rotating{
    from{
        transform: rotate(0);
    }
    to{
        transform: rotate(360deg);
    }
}
</style>


可以看到animation被自动补上了-webkit-

PS: postcss-preset-env 已经包含了autoprefixer,所以不用再安装autoprefixerpostcss-preset-env会根据package.jsonbrowserslist字段来处理对应的兼容性。

九、响应式单位处理

一般有两种,remvw,这里由于是移动端项目,所以推荐vw。设计稿的宽度标准是750px

  1. 运行npm i -D postcss-plugin-pxtoviewport,版本如下:
"postcss-plugin-pxtoviewport": "0.0.6",
  1. webpack.config.jspostcss-loader加入插件
{
    loader: 'postcss-loader',
    options: {
        postcssOptions: {
            plugins: [
                'postcss-preset-env',
                ['postcss-plugin-pxtoviewport', {
                    viewportWidth: 750, // 设计稿宽
                    unitPrecision: 3, // 计算结果不整除时要保留的小数位数
                    viewportUnit: 'vw', // 使用单位vw
                    minPixelValue: 1, // 小于这个值时不处理
                    mediaQuery: true // 允许在媒体查询中转换`px`
                }]
            ],
        },
    },
},

一般移动端页面是根据页面宽而高度自动等比缩放,所以,只需要所有尺寸都基于这个宽度即可。

  1. 修改App.vue样式代码如下:
...
    div{
        background: green;
        height: 150px;
    }
...


可以看到,150px被转换成了20vw,其计算公式100*(150/750),这样,就可以放心的按设计稿尺寸去写css了。

十、静态资源处理

接下来要处理的是图片、音视频、字体等的文件。引入方式有两种,一种是有.开头的路径(如'./''../'),需要url-loader file-loader raw-loader等这类加载器,另一种是无.开头的路径(如'/'''),需要copy-webpack-plugin拷贝,包含目录拷贝。

  1. 第一种,webpack5已经内置了资源加载模块,直接配置即可,无需安装loader。在webpack.config.jsmodule.rules数组中添加以下配置。
rules: [
...
{
    test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
    type: 'asset'
},{
    test: /\.(mp4|webm|ogg|mp3|wav|flac|aac|woff2?|eot|ttf|otf)(\?.*)?$/,
    type: 'asset/resource'
}]

type说明:
asset: 表示让webpack决定选择data uri(即base64)或uri形式;
asset/resoure: 表示使用uri形式。
点击了解更多参数

  1. 第二种,运行npm i -D copy-webpack-plugin安装插件,然后在webpack.config.js头入引入插件,然后在plugins数组中添加如下:
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
...
plugins:[
  new CopyWebpackPlugin({
    patterns: [{
        from: path.resolve(__dirname, 'public'),
        to: path.resolve(__dirname, 'dist'),
        toType: 'dir',
        filter: resourcePath => {
            return !/\.html$/.test(resourcePath)
        }
    }]
  })
]

上方配置是把public目录下的资源文件拷贝到dist,并且忽略掉.html文件。更多目录可以继续往patterns数组中添加。

  1. 运行npm start测试一下,把图片logo.png放在public中,然后在App.vue中添加<img src="logo.png"/>看看,然后再改成<img src="./logo.png"/>,此时会报错找不到资源,需要把logo.png移动到src目录。可自行测试不同大小的图片,看看各种情况生成的地址。
十一、接口代理

接口代理其实很简单,但很多不熟悉的人经常调试不通,因为没有真正理解,所以对不同的情况不知道怎么配置。

情况一:后端接口路径都是同一个开头,如:/api/login, /api/user, /api/xxx。这就好办了,简单配置如下即可。

devServer: {
    contentBase: './dist',
    proxy: {
        '/api': {
            target: 'http://localhost:3000',
            secure: true
        }
    }
},

在页面中发请求示例:

fetch('/api/login', {})
.then(res => res.json())
.then(res => {
  console.log(res)
})

后端最终接收到的请求是来自地址http://localhost:3000/api/login

情况二:后端接口路径开头不同,如: /login, /user, /xxx。这种有两种配置可以选择:

  • 在页面中开发时将所有接口都以特定路径开头,部署正式环境时再去掉,比如同样使用/api开头,则配置如下:
devServer: {
    contentBase: './dist',
    proxy: {
        '/api': {
            target: 'http://localhost:3000',
            secure: true,
            pathRewrite: {
                '^/api': ''
            }
        }
    }
},

在页面中发请求示例:

fetch('/api/login', {}).then(res => res.json()).then(res => {
  console.log(res)
})

后端最终接收到的请求是来自地址http://localhost:3000/login注意与情况一的区别,这里pathRewrite的配置就是把页面发请求时带的/api去掉,使后端真正得到的地址是http://localhost:3000/login

这种方式比较稳,不怕与页面路径有冲突,但有一个缺点就是发布正式时要把页面中的/api去掉,因为到正式环境时接口和页面地址是同一个域下,不需要代理。一般处理方式是:

// api.js
const apiBase = /^http\:\/\/localhost/.test(window.location.href) ? '/api' : ''
export function request(path, params){
  // TODO: build params
  return fetch(apiBase + path, params).then(res => res.json())
}

这样导出一个request函数来专门发送请求

import {request} from './api'
request('/login', {}).then(res => {
  console.log(res)
})

这样,只要发布正式的地址不是http://localhost访问就正常。

接下来说另一种配置

  • 保持地址原样,正式环境与开发环境一致,无需在页面处理
devServer: {
    contentBase: './dist',
    proxy: [{
        context: ['/login', '/user'],
        target: 'http://localhost:3000',
        secure: true
    }]
},

在页面中发请求示例:

fetch('/login', {}).then(res => res.json()).then(res => {
  console.log(res)
})

后端最终接收到的请求地址还是http://localhost:3000/login,这种配置虽然保持了开发环境还正式环境相同的页面代码,但也有缺点,如果开头不一样接口很多,有十几个或几十个,那得在context字段全部写上,如此,单面应用history模式或多页面开发很容易冲突,比如有一个用户信息html页面的访问地址是http://localhost:8080/user,而获取用户信息的接口是/user且是GET请求,那么访问http://localhost:8080/user时,不会进入html页面,而是被代理到了http://localhost:3000/user。所以,一般不推荐。

十二、多页面开发配置
  1. 先把目录结构改成如下:

    两个index.js的代码均如下:
import {createApp} from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

src/index/App.vue代码如:

<template>
  <div class="index">index page</div>
</template>
<script>
export default {
  data() {}
};
</script>
<style lang="scss"></style>

src/login/App.vue代码如:

<template>
  <div class="login">login page</div>
</template>
<script>
export default {
  data() {}
};
</script>
<style lang="scss"></style>
  1. 修改webpack.config.js以下几个地方:
...
entry: {
    index: './src/index/index.js',
    login: './src/login/index.js'
},
...
plugins: [
    new HtmlWebpackPlugin({
        title: 'index',
        template: './public/index.html',
        filename: 'index.html',
        chunks: ['index']
    }),
    new HtmlWebpackPlugin({
        title: 'login',
        template: './public/index.html',
        filename: 'login.html',
        chunks: ['login']
    }),
    ...
]

运行npm start就可看到页面index page,在浏览器输入地址http://localhost:8080/login.html就可以访问login页面。

这就是最基本的多页面配置。

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

推荐阅读更多精彩内容