文章内容太多,分为上下两部分,这里是上半部分。下半部分在《从零搭建前端工程(下)》,此篇的内容有:
1. 使用 webpack 打包(编译)vue
2. 使用 babel 处理 js
3. 使用 webpack-dev-sever 做热调试开发
产生这个的想法来自于某天接手一个项目,eslint 没配好,想着弄好格式化一下,一时之前忘了 cli 命令。
想一下,现在我们用的各种框架,都自带了脚手架工具,初始化后基本上不用怎么改造就可以上手开发。导致很多工程化的东西被人忽视了,就容易遇到问题时被卡住或者东找西找翻资料。
所以就想着自己从头搭建一个项目看看,遇到的一些配置、工具、命令都给记录下来,方便后面再遇到时候就不会生手了。
这里我就用 vue 来做基础框架,用 react 也一样,就一些编译工具的差别,大部分是一样的。要做的事有:
- 使用 webpack 打包(编译)vue
- 使用 babel 处理 js
- 使用 webpack-dev-sever 做热调试开发
- 使用 eslint + prettier 让代码健壮和优雅
- 使用 husky + lint-staged 强制增量各类检查
- 使用 @commitlint/cli 规范提交信息
- 使用 埋点(性能 + 错误) 让项目运行更好
零、文件初始化
准备一个和 vue 脚手架创建出来的初始工程一样。有:
src:项目源代码
src/main.js:工程入口文件,在这里做 vue 初始化,也是 webpack 的entry
src/App.vue:一个主页面
src/components/HelloWorld.vue:一个子组件
src/assets/logo.png:一张图片资源
src/public:公共资源文件夹,这里放的东西依样拷贝进打包里(除了index.html)
src/public/index.html:用来做单页面工程入口 html 的基础模板
然后初始化一下 npm 环境:
npm init -y
-y
就是创建一份默认的,不需要一步步问要填什么。直接生成空信息,有需要自己再去填就好了。
一、使用 webpack 打包(编译)
安装 webpack。webpack-cli 是为了可以在终端命令启动 webpack。如果使用 api 启动就不需要安装这个。
npm install -D webpack webpack-cli
然后新建一份 webpack.config.js
在项目根目录。这是 webpack 的默认指定配置文件。用过老一点的 vue 工程的话,可能会有印象工程里有三份类似的配置 webpack.base.config.js/webpack.dev.config.js/webpack.prod.config.js,因为开发用和打生产包有些东西不一样,所以分了两份配置,再把共同的抽取成一份 base。后面的 vue 工程就看不到相关 webpack 配置了。全部都封装了起来。定义了一份 vue.config.js
做扩展入口。我们可以用命令打它里面的默认配置给打印出来,以供参考学习。
vue inspect > output.js
这点我觉得比 react 好。react 没有留扩展入口。如果自己想要 DIY 一些配置,要把配置打出来,而且过程不可逆。虽然一般可能也不太需要自己再加什么。或许也有别的方法,可能我没了解到。
webpack 的配置用法这里就不赘述了。直接把 webpack.config.js 内容展示出来,再来解释都是些啥。
const { resolve } = require('path');
const webpack = require('webpack');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const cwd = process.cwd();
const publicDir = resolve(cwd, 'public');
const isDev = process.env.NODE_ENV === 'development';
module.exports = {
mode: 'development',
devtool: isDev ? 'source-map' : false,
entry: {
main: resolve(cwd, 'src/main.js'),
},
output: {
path: resolve(cwd, 'dist'),
filename: '[name].js', // 打包后输出文件的文件名
},
devServer: {
static: {
directory: publicDir, // 指定静态资源的路径
},
compress: true, // 是否压缩
port: 8080, // 指定使用端口
hot: true, // 是否热更新,那肯定啦...
open: true, // 编译完成打开浏览器
client: {
logging: 'error', // 终端显示错误即可
overlay: { // warnings 类型不要挡着页面,error才展示为浏览器页面
errors: true,
warnings: false,
},
},
// proxy: { // 配置代理
// '/api': {
// target: 'http://localhost:3000',
// changeOrigin: true,
// },
// },
},
// optimization: {
// splitChunks: {
// chunks: 'all',
// // minChunks: 3,
// // minSize: 0,
// cacheGroups: {
// default: {
// priority: 1,
// reuseExistingChunk: true,
// enforce: true
// }
// }
// }
// },
module: {
rules: [
{
test: /\.js$/, // 解析 js 文件
exclude: /node_modules/,
use: 'babel-loader',
},
{
test: /\.vue$/, // 解析 vue 文件
exclude: /node_modules/,
use: 'vue-loader', // 17版本会导致 css 打包报错
},
{
test: /\.css$/, // 添加对样式表的处理
use: [
// 请只在生产环境下使用 CSS 提取,这将便于你在开发环境下进行热重载
// 'vue-style-loader' 与 'style-loader' 类似,会把处理好的 css 内容注入 js 里
// 所以开发用 style-loader,生产用 MiniCssExtractPlugin.loader
isDev ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
],
},
{
test: /\.less$/, // 使用 less sass 这些要额外声明处理规则
use: [
isDev ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
],
},
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
// 新版本的file-loader选项esModule默认为true,
// 需要将其设置为false,否则打包后src为[object Module]
esModule: false,
limit: 8192,
},
},
],
},
],
},
resolve: {
alias: {
'@': resolve(cwd, 'src'),
},
},
plugins: [
// new BundleAnalyzerPlugin(),
// 参考官方文档 https://vue-loader.vuejs.org/migrating.html#a-plugin-is-now-required
// Vue-loader在15.*之后的版本都是 vue-loader的使用都是需要伴生 VueLoaderPlugin的,
new VueLoaderPlugin(),
// 编译时替换全局变量
new webpack.DefinePlugin({
BASE_URL: JSON.stringify('./'),
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
}),
new ESLintPlugin({
extensions: ['.js', '.vue'], // Default: 'js'
}), // https://eslint.org/docs/latest/integrate/nodejs-api#-new-eslintoptions
new MiniCssExtractPlugin({
filename: 'style.css',
}),
// 用来将打包的 js 注入到 index.html
new HtmlWebpackPlugin({
inject: 'body',
template: resolve(publicDir, 'index.html'),
}),
// 把 public 的文件拷贝过去 dist 文件
new CopyWebpackPlugin({
patterns: [
{
from: publicDir,
globOptions: {
ignore: [
'**/index.html', // html 由 HtmlWebpackPlugin 生成,不需要拷贝
'**/.DS_Store',
'**/Thumbs.db',
],
},
},
],
}),
],
};
1 使用的 loader
基础的 entry
和 outpout
就不说了。我们知道 webpack 是一个模块解决方案,它就是把所有东西打包(编译)成能运行的 js。所有东西,种类很多,所以我们要告诉它各式各样的文件类型用对应的 loader 去处理,包括 js,也要相应 loader 去处理。所以在 module.rules
那里,可以看到指定了用 babel-loader
去处理 js 文件,用 vue-loader
去处理 vue 文件。
1.1 vue-loader 干了啥
这里简单介绍一下 vue-loader 干了啥。我们知道,vue 文件是 <template> <script> <style> 三部分组成,里面还夹杂了 vue 自身的模板语法,正常浏览器当然不认识这么个写法。所以要把 vue 文件给编译成浏览器喜闻乐见的 js。vue-loader 先把这三部分拆分,分成三份文件,这是编译过程中产生的,并不会真的生成相应的文件。<template> 部分经过 ast 处理后也变成 js 了,用 babel-loader 去接着处理。<style> 则变成 css 文件,所以要指定 style-loader
和 css-loader
去处理。
注意 vue-loader 还要搭配使用 VueLoaderPlugin
1.2 对样式的处理
上面提到,对于样式,我们使用了 style-loader
和 css-loader
。但现在我们都会使用 less sass 等 css 预处理语言。当我们像 <style lang="less">
这样加上 lang 声明后,vue-loader就会从生成一份 .css 文件变成生成一份 .less 文件了。此时加上多一个相应的 loader,如 less-loader
、sass-loader
先处理就行了。
注意这里还用到了 MiniCssExtractPlugin.loader,这是一个来自于插件 mini-css-extract-plugin
的一个附属插件,可以看到同时也使用了它的 plugin 部分。作用是:
将里面的所有 class 给抽取出来,生成一份单独的 style 文件。一来减轻 js 产物的体积。二来 css 改动频率应该比不上 js 的改动,可以针对做不一样的缓存策略。
对其他资源的处理
一般都是用 url-loader
处理。一般都是小图标会转换成 base64 引入。稍微有点体积的,也不会参与编译了,都是上传到文件服务器,拿资源的 http 地址来使用。
2 使用的 plugin
上面提到的 VueLoaderPlugin 和 MiniCssExtractPlugin 就不赘述了。
2.1 替换全局变量
有时候,我们经常有不同环境要注入不同变量的,比如开发与生产请求不一样的地址之类的。可以借助 webpack.DefinePlugin
将全局变量注入代码里。代码里直接写 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
这样,打包的时候,就会把这个字符给替换成相应的值,这样我们就把外部的值给注入到 web 应用里。要注意的是,自己声明的全局变量,对于 eslint 或 tslint 是不认识,可能会报错。这时候需要在这两者的配置里声明这是全局变量,不要报错。
2.2 加入 eslint 做编译时语法检测
eslint 的具体用法下文再讲。这里 webpack 也需要集成 eslint,虽然在开发时候编辑器应该配好提醒,但还是需要在编译工具再做一层拦截兜底。以前是用 eslint-loader
的,现在发现改成用 eslint-webpack-plugin
了,不过用法依然简单,毕竟复杂的是 eslint 本身,而不是 webpack 怎么集成它。具体配置可看介绍。
2.3 生成单页面用的 html
单页面框架最后生成的是 js,也得有一个 html 页面作为载体。需要注入打包出来的 js,然后放到产出的文件夹里。用的是 html-webpack-plugin
插件。需要指定一个基础模板,就是 public 下的 index.html 啦。然后可以配置生成的名字啊,注入的 js 要放在那里什么的。还挺多不错的配置的,详情可看介绍。
2.4 拷贝静态资源文件
上面说了,public 的东西除了 index.html 由 html-webpack-plugin 生成外,其他的会依样拷贝到产出包里。这里用 copy-webpack-plugin
插件。当然,就一个拷贝动作,自己写点 node 代码也可以。
2.5 分析包体积大小
庞大的项目会打出巨型的生产包。这时候我们需要知道到底是到底是哪里体积大了,比如是否有可替代的小体积依赖包等。用 webpack-bundle-analyzer
插件会生成一份分析结果的 html,里面详细列出了包的各个组成。
3 其他点
再介绍下其他的一些配置点:
devtool:编译后的代码已经和我们写的源码大相径庭,这个字段用来配置 source-map 的类型,详情可看官网介绍。
要注意是,source-map的类型会影响编译速度。
resolve.alias:给文件引用起别名。我们在写代码的时候,引进别的文件,要看清楚相对路径,写一大堆点点点。有了这个就可以不管三七二十一,比如根据配置 '@': resolve(cwd, 'src')
,就可以直接了当的写:import xxx from '@/yyy/zzz'
。webpack 会在编译时候自动去替换路径。
当然也不是百分百适用,比如层级特别深的 a.js 要引用同层级的 b.js,那不如就用相对路径 ./b.js
就可以了。个人觉得超过两层,如 ../../b.js
, 就用 alias。
关于 resolve 还有很多实用的配置,详情可看官网介绍
webpack 的配置还很多也复杂,以前还被笑称前端有个分类为 webpack 工程师。这里只示例一些基本的。真正的项目还是会使用更多的配置。甚至,还可能需要自己开发 loader 和 plugin。这里可以看一下之前写的文章《webpack的配置》。里面示例了 loader 和 plugin 的基础写法和其他的一些配置介绍。
二、使用 babel 处理 js
虽然在 webpack 里就一个 babel-loader
完事,但是 babel 是一个很复杂而厉害的东西。值得单独说一说。
官网的概念就不说了,我笼统说下自己理解:众所周知,ECMAScript的标准制定,是领先于浏览器的实现的。有时候定好方法,浏览器不支持也是白搭。babel 就是用来抹平这部分差距。把各类超前的 js 编译成当前浏览器支持的语法。使得开发者也可以放心愉快地使用一些新的 api。
所以我们先安装 babel 一系列东西。
npm install -D @babel/core @babel/preset-env @babel/plugin-transform-runtime babel-loader
@babel/ 这种 monorepo 仓库是 7 版本开始的。以前是 babel-core。*
在项目根目录下创建一份 babel.config.js
,先把配置贴一贴:
module.exports = {
presets: [
[
'@babel/preset-env',
{
// targets: {
// chrome: 100,
// },
useBuiltIns: 'usage',
corejs: 3,
},
],
],
plugins: [
'@babel/plugin-transform-runtime',
],
};
配置有两个东西:presets
和 plugins
。
1 presets
告诉 babel,要编译到哪个标准的版本。据说以前是带有明显版本号的依赖名:babel-preset-es2016
。这样搞不好每次新标准就要改新包了。干脆,就用一个 @babel/preset-env
代表最新。对于 vue 和 react 也推出了自己的 presets,@vue/babel-preset-app
和 @babel/preset-react
。
这个写法也是比较特别,首先最外层是个数组,这个没问题。数组元素的话,如果没有参数的话,就是一个普通的字符串。如果有参数,就变成一个数组,第一个元素还是该 preset。第二元素就变成一份 json 配置。如上面示例,就是个二维数组。这个配置,有几个参数会比较常见到。
1.1 target
指定到哪个浏览器版本,比如 100 版本的 chrome 是支持 async await 和 optional chaining 的,那就不会去转译这个语法。
1.2 useBuiltIns
有三个值:”usage” | “entry” | false, defaults to false。false 就是不启用。
“usage”: 必须在入口处主动引入(只能引一次) @babel/polyfill(已经包含core-js) 或者 core-js,就会注入一系列垫片函数,全部引入。
“entry”: babel 会判断到底使用了哪些当前指定浏览器版本不支持的方法,按需注入那些垫片函数。
1.3 corejs
这个功能的核心库,可以不声明,默认用的是 2 版本。想要用 3 版本主动安装 core-js 依赖库 3 版本以上,同时要像例子那样声明 corejs: 3
。当然 3 版本比 2 版本功能丰富,对新功能支持就更好。
关于 presets 详情,可看官网介绍。
2 plugins
有时候,一些新语法新方法没法支持,就得自己注入一些垫片函数来支持。比如对于 async await 的支持,是注入了一个 regeneratorRuntime(大致是这个名字) 的方法来处理。我们经常看到的是这个插件:@babel/plugin-transform-runtime。不引入是不是就不行?其实不引入也是可以支持的。
@babel/preset-env
内置了 @babel/plugin-transform-regenerator
, 会对 async await 进行转译,但是是在每个模块的作用域给加上处理函数,这样可能造成多个冗余了这个处理函数。所以一般都会使用 @babel/plugin-transform-runtime
,它是把处理函数给抽取成单独的模块函数,全局一份。
如图,是把整个方法写在这份文件里。一份 js 就有一份。在 webpack 的处理下,每一份 js 都是一个闭包模块。然后每个模块代码都有一样的这个方法。
如图,可以看到是变成引用了一个依赖方法,那么在 webpack 处理下,这个就会是同一个第三方方法,就只会有一份代码。
注意有些旧版本 babel 没有注入处理函数,会报错: regeneratorRuntime is not defined。此时也要自己手动引入该函数,老版本叫 babel-plugin-transform-runtime,与使用的 babel-core、@babel/core 相呼应。
3 一些 cli 命令
# 基本的单文件编译并指定输出文件及生成 source-map
babel src/index.js --out-file(-o) dist/lib.js --source-maps(-s)
# 编译一份文件夹内容并输出到一份文件夹
babel src --out-dir(-d) lib
# 编译整个 src 目录下的文件并将输出合并为一个文件。
babel src --out-file script-compiled.js
# 忽略规范和测试文件
babel src --out-dir lib --ignore "src/**/*.spec.js","src/**/*.test.js"
# 复制不需要编译的文件
babel src --out-dir lib --copy-files
# 自定义配置文件路径
babel --config-file /path/to/my/babel.config.json
三、使用 webpack-dev-sever 做热调试开发
这个大家也是耳熟能详了。现代化前端工程几乎离不开编译这个操作,如果每次改完代码,手动执行下编译操作,然后刷新页面,这开发体验也太灾难了。好在,还是 webpack,帮我们推出了 webpack-dev-server,可以虚拟打包产物放在内存,然后监听文件的修改,有修改就只更新那部分文件,大大提高了开发效率。
用法也很简单,在前面两个 webpack 工具前置安装完后,再安装这个。
npm install -D webpack-dev-server
然后搭配 webpack 启动
npx webpack serve
# 或
npx webpack-dev-server
还可以用 api 方式启动,用于需要判断众多环境启动不一样的服务的时候,随便新建一份 js:
const { resolve } = require('path');
const Webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const webpackConfig = require(resolve(process.cwd(), 'webpack.config.js'));
const compiler = Webpack(webpackConfig);
const devServerOptions = { ...webpackConfig.devServer };
const server = new WebpackDevServer(devServerOptions, compiler);
const runServer = async () => {
console.info('Starting server...');
server.startCallback(() => {
console.info(
`Successfully started server on http://localhost:${webpackConfig.devServer.port}`
);
});
// 或者 await server.start();
};
runServer();
用法简单,因为对于编译配置比如 loader、plugin 这些本身就和 webpack 如出一辙。用的同一份 webpack.config.js,只要用环境变量区分 production 和 development 就行了。要了解的是,针对于 webpack-dev-server 的配置。看看 devServer
字段的配置。上面的例子已经加了注释介绍。事实上还有很多功能,比如 proxy 可以访问到请求和响应对象,方便做一些接口的统一处理。这些都视实际情况而定。
更新
2023-08-24 更新
webpack5 对于资源文件的处理有更新,详情参阅《webpack5 下 background url 图片打包失败》。
以下内容请移步《从零搭建前端工程(下)》阅读
4. 使用 eslint + prettier 让代码健壮和优雅
5. 使用 husky + lint-staged 强制增量各类检查
6. 使用 @commitlint/cli 规范提交信息
7. 使用 埋点(性能 + 错误) 让项目运行更好