webpack5+vue3+typescrip实现多入口模式
最近公司项目要做一个H5的项目,主要支持APP手机端与安卓TV端,UI给的设计分别是375px和1920px,每个H5页面十一单独的业务,相互之间没有业务往来,所以也不需要Vue-router,于是决定用webpack5从头开始搭建一个Vue3的多入口开发环境
项目地址: github.com/Cekun/webpa…
项目目录结构如下:
├─build
│ ├─webpack.base.conf.js
│ ├─webpack.dev.conf.js
│ ├─webpack.prod.conf.js
│ ├─webpack.rules.conf.js
├─node_modules
├─public
| |-favio.io
└─src
| ├─api
| ├─assets
| ├─components
| ├─filters
| ├─plugins
| ├─router
| ├─style
| ├─utils
| ├─views
| |-App.vue
| |-main.ts
|-.env.dev
|-.env.test
|-.env.prod
|-.gitigore
|-babel.config.js
|-package.json
|-postcss.config.js
复制代码
开始从0使用webpack5搭建一个完整Vue3开发环境
环境
webpack 5 运行于 Node.js v10.13.0+ 的版本。
本次使用的版本号:
node: 14.15.0
webpack: ^5.70.0,
webpack-cli: ^4.9.2,
复制代码
创建目录并初始化package.json文件
$ mkdir webpack5-vue3-ts & cd webpack5-vue3-ts
$ npm init
复制代码
// package.json
{
"name": "webpack5-vue3-ts",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
}
复制代码
配置环境变量
使用dotenv
来按需加载不同的环境变量,VUE CLI3的环境变量也是使用的这个插件
-
安装
dotenv
插件$ cnpm i dotenv -D 复制代码
-
根目录配置变量文件
-
.env.dev
NODE_ENV = 'development' VUE_APP_BASE_API = '/api' VUE_APP_SHOWCONSOLE = true 复制代码
-
.env.prod
NODE_ENV = 'production' VUE_APP_BASE_API = 'http://kkiot.kkapp.com/' VUE_APP_SHOWCONSOLE = false 复制代码
- .env.test
NODE_ENV = 'production' VUE_APP_BASE_API = 'http://kkiot.kkapp.com/' VUE_APP_SHOWCONSOLE = false 复制代码
-
安装webpack三套件及其相关模块
npm i webpack webpack-cli webpack-dev-server -D
npm i webpack-merge -D
复制代码
注意:
-D 等价于 –save-dev; 开发环境时所需依赖
-S 等价于 –save; 生产环境时所需依赖
创建文件
./build/webpack.base.conf.js
; (基础配置)
./build/webpack.dev.conf.js
; (开发配置)
./build/webpack.prod.conf.js
; (生成配置)
./build/webpack.rules.conf.js
; (loader配置)
// webpack.base.conf.js
const path = require('path'),
webpack = require("webpack"),
rules = require("./webpack.rules.conf"),
envMode = process.env.envMode,
prefixRE = /^VUE_APP_/; // 正则匹配以 VUE_APP_ 开头的 变量
require("dotenv").config({ path: `.env.${envMode}` });
let env = {};
// 只有 NODE_ENV,BASE_URL 和以 VUE_APP_ 开头的变量将通过 webpack.DefinePlugin
// 静态地嵌入到客户端侧的代码中
for (const key in process.env) {
if (key == "NODE_ENV" || key == "BASE_URL" || prefixRE.test(key)) {
env[key] = JSON.stringify(process.env[key]);
}
}
module.exports = function (prodMode) {
return {
mode: "development",
entry: path.resolve(__dirname, '../src/main.js'), // 入口
module: {
rules: rules(prodMode),
},
plugins: [
new webpack.DefinePlugin({ // 定义环境变量
"process.env": { ...env },
}),
]
}
}
复制代码
// webpack.dev.conf.js
const path = require('path'),
{ merge } = require('webpack-merge')
webpackConfigBase = require('./webpack.base.conf')
let webpackConfigDev = {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
module: {},
plugins: [],
// webpack-dev-server 会从 output.path 中定义的目录为服务提供 bundle 文件,
// 即,文件将可以通过 http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename] 进行访问。
devServer: {
port: 3000,
hot: true,
open: true,
historyApiFallback: true,
// 设置代理,用来解决本地开发跨域问题,
// 如果设置了代理,那你本地开发环境的axios的baseUrl要写为 '' ,即空字符串
proxy: {
'/api': {
secure: false,
changeOrigin: true,
target: 'www.apishop.net', // 接口测试网站
pathRewrite: { '^/api': '' }
},
}
},
}
module.exports = merge(webpackConfigBase(false), webpackConfigDev)
复制代码
//webpack.prod.conf.js
const path = require("path"),
{ merge } = require("webpack-merge"),
webpackConfigBase = require('./webpack.base.conf')
const webpackConfigProd = {
mode: 'production',
output: {
path: path.resolve(__dirname, "../dist"),
filename: "./[name]/js/[name].[contenthash:8].js", // 根据文件名打包输出到指定文件目录
publicPath: '../',
clean: true, // Clean the output directory before emit.
// assetModuleFilename: "./assets/[name].[hash:8][ext]"
},
plugins: [],
module: {
rules: []
},
}
module.exports = merge(webpackConfigBase(true), webpackConfigProd)
复制代码
// webpack.rules.conf.js
const path = require('path')
module.exports = function (prodMode) {
return [ ]
}
复制代码
修改package.json
{
...
"scripts": {
"dev": "cross-env envMode=dev webpack server --config ./build/webpack.dev.conf.js --color",
"build": "cross-env envMode=prod webpack --config ./build/webpack.prod.conf.js --color",
"build:test": "cross-env envMode=test webpack --config ./build/webpack.prod.conf.js --color"
},
...
}
复制代码
- –config或-c: 提供 webpack 配置文件的路径,例如 ./webpack.config.js
- –mode:配置环境也可写在配置文件里 不配置mode 默认production模式打包
- –progress: 启用在构建过程中打印编译进度
- –color: 启用控制台颜色
- –watch或-w: 监听文件变化
运行打包脚本npm run build
看到webpack运行并且打包成功了。
配置核心功能
ES6转ES5
由于有些浏览器无法解析 ES6+ 等高级语法,故需要将其转化为浏览器能够解析的低版本语法
npm i @babel/core @babel/preset-env babel-loader -D
npm i core-js -S
复制代码
loader配置
// webpack.rules.conf.js
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
}
},
]
复制代码
Babel 配置文件
Babel的配置文件是Babel执行时默认会在当前目录寻找的文件,主要有.babelrc,.babelrc.js,babel.config.js和package.json。它们的配置项都是相同,作用也是一样的,只需要选择其中一种。
推荐使用后缀名是js配置文件,因为可以使用js做一些逻辑处理,适用性更强。
// babel.config.js
const presets = [
["@babel/preset-env", {
"useBuiltIns": 'usage', // 这里配置usage 会自动根据你使用的方法以及你配置的浏览器支持版本引入对于的方法。
"corejs": "3.21.1" // 指定 corejs 版本
}]
]
const plugins = [
]
module.exports = {
plugins,
presets
}
复制代码
产出HTML
安装 html-webpack-plugin
插件处理 index.html 文件,此插件的功能是根据提供的模板文件,自动生成正确的项目入口文件,并把 webpack 打包的 js 文件自动插入其中
npm i html-webpack-plugin -D
复制代码
plugins配置
基本配置:
// webpack.base.conf.js
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../src/index.html'),
filename: 'index.html',
title: 'webpack5+vue3',
minify: {
html5: true, // 根据HTML5规范解析输入
collapseWhitespace: true, // 折叠空白区域
preserveLineBreaks: false,
minifyCSS: true, // 压缩文内css
minifyJS: true, // 压缩文内js
removeComments: false // 移除注释
},
files: prodMode ? cdn.prod : cdn.dev //CDN引入文件配置
}),
复制代码
由于我们是多入口,有多个HTML文件,现在对上面代码做改造,多入口结构目录结构如下:
└─src
| ├─views
| ├─app-airConditioner
| | ├─index.ts
| | ├─index.html
| ├─app-curtain
| ├─app-switch
复制代码
// 重写new HtmlWebpackPlugin()
// webpack.base.conf.js
const viewDir = path.resolve(__dirname, "../src/views"),
dirs = fs.readdirSync(viewDir); // 业务页面目录
let entry = {}, //入口文件
htmlPlugins = [];
for (let i = 0; i < dirs.length; i++) {
const dir = dirs[i];
entry[dir] = viewDir + "/" + dir + "/index.ts";
htmlPlugins.push(
new HtmlWebpackPlugin({
filename: `./${dir}/index.html`,
template: "./src/index.html",
chunks: [dir], // HTML只引入对应chunks的JS文件
title: dir,
minify: {
collapseWhitespace: false, // 去掉空格
removeComments: true, // 去掉注释
},
// files: cdn.prod,
})
);
}
module.exports = function (prodMode) {
return {
mode: "development",
entry, // 改为多入口
plugins: [
...htmlPlugins
]
...
}
}
复制代码
注意: 配置动态网页标题时,需将模板中的 <title>
标签里的内容改成 <%= htmlWebpackPlugin.options.title %>
CDN引入js
<% for (var i in
htmlWebpackPlugin.options.files&&htmlWebpackPlugin.options.files.js) { %>
<script src="<%= htmlWebpackPlugin.options.files.js[i] %>"></script>
<% } %>
复制代码
添加 css 和 sass 支持
npm i style-loader css-loader -D
npm i node-sass sass-loader -D
npm i autoprefixer postcss-loader -D
复制代码
loader配置
//webpack.rules.conf.js
{
test: /\.(css|scss|sass)$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
'sass-loader'
]
}
复制代码
loader 从右到左(或从下到上)地取值(evaluate)/执行(execute)。在上面的示例中,从 sass-loader 开始执行,最后以 style-loader 为结束。
配置 alias 别名
创建 import 或 require 的别名,来确保模块引入变得更简单
// webpack.base.conf.js
resolve: {
alias: {
"@": path.resolve(__dirname, "../src"),
assets: path.resolve(__dirname, '../src/assets/'),
img: path.resolve(__dirname, '../src/assets/img'),
utils: path.resolve(__dirname, '../src/utils'),
api: path.resolve(__dirname, '../src/api'),
},
},
复制代码
**注意:**引入图片资源需要在别名前加 ~,不然会报找不到资源的错误,比如:
<img class="goods-img" src="~img/switch_img.png" alt="开关" style="width: 114px">
复制代码
body {
background-image: url(~img/switch_img.png)
}
复制代码
处理图片等静态资源
Webpack5 之前我们处理静态资源比如PNG 图片、SVG 图标等等,需要用到url-loader,file-loader,raw-loader。Webpack5 提供了内置的静态资源构建能力,我们不需要安装额外的 loader,仅需要简单的配置就能实现静态资源的打包和分目录存放。这三个loader在github上也停止了更新。
webpack5使用四种新增的资源模块(Asset Modules)替代了这些loader的功能。
asset/resource 将资源分割为单独的文件,并导出url,就是之前的 file-loader的功能.
asset/inline 将资源导出为dataURL(url(data:))的形式,之前的 url-loader的功能.
asset/source 将资源导出为源码(source code). 之前的 raw-loader 功能.
asset 自动选择导出为单独文件或者 dataURL形式(默认为8KB). 之前有url-loader设置asset size limit 限制实现。
//webpack.rules.conf.js
{
test: /\.(eot|svg|ttf|woff|)$/,
type: "asset/resource",
generator: {
// 输出文件位置以及文件名
filename: "fonts/[hash:8].[name][ext]",
},
{
test: /\.(png|jpe?g|svg|gif)$/,
type: "asset/inline",
},
},
复制代码
静态资源输出到根目录
npm i copy-webpack-plugin -D
复制代码
当某些文件(比如网站图标、第三方库等)不需要经过webpack打包处理而直接使用,这里我们可以使用 copy-webpack-plugin
这个插件,在构建的时候,将 public/ 的静态资源直接复制到 dist根目录下
//webpack.prod.conf.js
new copyWebpackPlugin({
patterns: [{
from: path.resolve(__dirname, "../public"),
to: './',
globOptions: {
dot: true,
gitignore: true,
ignore: ["**/index.html*"],
}
}]
}),
复制代码
提取样式文件
本插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。此处我们会根据业务需求把css提取到对应的业务文件夹下
npm i mini-css-extract-plugin -D
复制代码
//webpack.prod.conf.js
plugins: [
//...
new miniCssExtractPlugin({
filename: './[name]/css/[name].[contenthash].css',
chunkFilename: './css/[id].[contenthash].css',
})
],
复制代码
修改webpack.rules.conf.js
{
test: /\.(css|scss|sass)$/,
use: [
!prodMode ? 'style-loader' : {
loader: MiniCssExtractPlugin.loader,
options: { publicPath: '../' }
},
'css-loader',
'postcss-loader',
'sass-loader'
]
},
复制代码
识别 .vue文件
npm i vue-loader@next @vue/compiler-sfc -D
npm i vue@next -S
复制代码
注意:
- vue-loader:它是基于 webpack 的一个的 loader 插件,解析和转换 .vue 文件,提取出其中的逻辑代码 script、样式代码 style、以及 HTML 模版 template,vue 3.x需要安装vue-loader@next。
- @vue/compiler-sfc: Vue 2.x 时代,需要 vue-template-compiler 插件处理 .vue 内容为 AST , Vue 3.x 则变成 @vue/compiler-sfc
修改webpack配置
// webpack.rules.conf.js
rules: [
{
test: /\.vue$/,
use: [
'vue-loader'
]
}
]
//webpack.base.conf.js
const { VueLoaderPlugin } = require('vue-loader/dist/index');
plugins: [
new VueLoaderPlugin()
]
复制代码
识别.ts文件
npm install typescript ts-loader -D
复制代码
安转tsc ,初始化tsconfig.js
{
"compilerOptions": {
"experimentalDecorators": true,
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": false,
"module": "commonjs",
"downlevelIteration": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"strict": true,
"jsx": "preserve",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2020",
"dom"
],
// // 用于设置解析非相对模块名称的基本目录,相对模块不会受到baseUrl的影响
"baseUrl": ".",
"paths": {
// 用于设置模块名到基于baseUrl的路径映射
"@/*": [
"src/*"
],
}
},
"exclude": [
".history",
"node_modules",
"src/**/*.",
],
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"@types"
]
}
复制代码
修改 .webpack.rules.conf.js
{
test: /.ts$/,
use: [
{
loader: "ts-loader",
options: {
appendTsSuffixTo: [/.vue$/],
},
},
],
},
复制代码
添加typescript识别vue, 图片文建@/types/index.d.ts
declare module '*.vue' {
import { ComponentOptions } from 'vue'
const component: ComponentOptions;
export default component;
}
declare module '*.png'
复制代码
以免编译器提示不识别错误
rem适配
npm i postcss-loader postcss-pxtorem -D
复制代码
新建postcss.config.js文件
module.exports = ({ file }) => {
const designWidth = /tv-/.test(file.toLowerCase()) ? 19.2 : 37.5; // 区分APP与TV设计稿
return {
plugins: {
autoprefixer: {
Browserslist: ["Android >= 4.0", "iOS >= 7", "chrome > 31"],
},
"postcss-pxtorem": {
rootValue: designWidth, //19.2, // 数字|函数)表示根元素字体大小或根据input参数返回根元素字体大小
propList: ["*"], // 使用通配符*启用所有属性
mediaQuery: true, // 允许在媒体查询中转换px
selectorBlackList: [".norem"], // 过滤掉.norem-开头的class,不进行rem转换
},
},
};
};
复制代码
去掉生产环境console.log
使用TerserWebpackPlugin来进行去除console.log
,压缩JS,webpack5之后自带最新的terser-webpack-plugin
,无需再重新安装原文点我点我
// webpack.prod.conf.js
const TerserPlugin = require('terser-webpack-plugin')
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
// 多进程
parallel: true,
//删除注释
extractComments: false,
terserOptions: {
compress: { // 生产环境去除console
drop_console: true,
drop_debugger: true,
},
},
})
]
}
复制代码
编译缓存
Webpack5 内置 FileSystem Cache
能力加速二次构建,可以通过以下配置来实现
cache: {
type: 'filesystem',
// 可选配置
buildDependencies: {
config: [__filename], // 当构建依赖的config文件(通过 require 依赖)内容发生变化时,缓存失效
},
name: '', // 配置以name为隔离,创建不同的缓存文件,如生成PC或mobile不同的配置缓存
...,
},
复制代码
配置好后第二次构建速度快的飞起。
注意事项:
- cache 的属性 type 会在开发模式下被默认设置成
memory
,而且在生产模式中被禁用,所以如果想要在生产打包时使用缓存需要显式的设置。 - 为了防止缓存过于固定,导致更改构建配置无感知,依然使用旧的缓存,默认情况下,每次修改构建配置文件都会导致重新开始缓存。当然也可以自己主动设置
version
来控制缓存的更新。
集成 Vant
cnpm i vant@next -S
复制代码
-
按需引入
yarn add babel-plugin-import -D 复制代码
-
修改配置
// babel.config.js const plugins = [ ['import', { libraryName: 'vant', libraryDirectory: 'es', style: true }, 'vant'] ] 复制代码
-
vant 适配rem
修改postcss.config.js
module.exports = ({ file }) => {
// const designWidth = file.includes(path.join('node_modules', 'vant')) ? 375 : 750;
return {
plugins: {
autoprefixer: {
Browserslist: ["Android >= 4.0", "iOS >= 7", "chrome > 31"],
},
"postcss-pxtorem": {
rootValue: 37.5, // 数字|函数)表示根元素字体大小或根据input参数返回根元素字体大小
propList: ["*"], // 使用通配符*启用所有属性
mediaQuery: true, // 允许在媒体查询中转换px
selectorBlackList: [".norem"], // 过滤掉.norem-开头的class,不进行rem转换
},
},
};
};
复制代码
结语
由于此处我们的项目的每个业务(设备控制页)即为一个单独H5页面,彼此之间不存在业务上的交互往来,也不会有路由跳转,故此处没有使用vue-router,也没有引入vuex作为状态管理,后续开发中有使用再引入