使用 Webpack
本章对应视频讲解如下
边看视频讲解,边学习教程
前端工程化
参考
https://segmentfault.com/a/1190000040246060
https://juejin.cn/post/6917447582985748493
早期的前端开发就是实现web页面,主要是开发 HTML、CSS 文件 和一些简单的js代码
后来前端技术高速发展更新,往往前端项目的功能越来越多,开发工作量越来越大。
这时,前端工程化的概念被提出了,主要解决代码冗余,项目可维护性,提升版本迭代速度等等一系列的问题。
总之是要 提高 开发效率,减少维护成本。
前端工程化要做的事情 包括: 团队代码编写规范、开发流程规范(包括需求、开发、测试、发布等)、配置管理规范、项目工具规范 等等
我们这里介绍一下 前端工程化 常用的 项目构建工具 webpack
为什么需要构建工具
以我们项目实战为例,到 模块化结束 这个阶段, 我们发现面临如下问题:
-
源文件jsx和js混在项目中
代码目录下面 同时有 customer.jsx 和 customer.js ,很容易混淆,一不小心就改错了
而且,也给代码开发导航带来一些问题
比如,查看 order.jsx 里面的依赖的 Widgets库组件的定义,会跳转到编译后的代码
crud.js而不是源代码crud.jsx因为代码里面是这样写的
import {CRUD} from "./crud.js"而不是
import {CRUD} from "./crud.jsx" -
修改完编译麻烦
我们现在每次修改代码,都要手工执行命令编译修改过的文件, 比较麻烦。
批处理一起执行,会把没有修改的也编译一遍,浪费时间。
能不能不需要手工执行命令,修改了自动编译好,即时反馈在网页程序上?
-
多个模块加载的性能问题
我们的代码分解为多个模块,运行时就要多次网络请求模块文件
在生产环境下,多次导入一个个小文件,性能会比较差。
希望在 正式发布到生产环境 的时候,把一些代码文件合并为一个较大的文件, 减少网络访问次数。
上面的问题,通过前端项目构建工具,可以比较好的解决
我们这里介绍一下 前端工程化 常用的 项目构建工具 webpack
webpack 简介
webpack 是构建工具,功能通常包括 编译 和 打包
浏览器能直接运行的 只有 HTML、CSS、JS 3种类型的语言内容
但是我们的项目往往包含其它类型的代码内容,所以 需要先 编译 为浏览器能直接运行的 HTML、CSS、JS
这些内容包括:
-
转化无法被浏览器直接识别的JS代码
典型的就是我前面学习过的JSX代码
另外还可能 包括 ECMAScript新规范,目标浏览器还不支持 的js代码 ,
而且, 使用它,代码可以直接 import jsx类型的文件,而不是必须 import 转化好的js类型的文件。因为 webpack 会维护 jsx 和 js 的对应关系,最终产生的代码
-
转化无法被浏览器直接识别的CSS代码
比如:SASS/LESS
-
转化一些框架定义的HTML模板
比如 svelte、mustache等
项目构建往往还包含打包,主要包括:
-
依赖打包
将同步依赖的文件打包在一起,减少HTTP请求数量;
-
文件压缩
减少文件体积,提高下载效率;
所以 webpack最终产生的js文件,通常是多个源文件经过处理过的最终合并结果
注意,webpack 构建的依赖关系是根据模块的导入关系决定的,
所以,项目代码需要先模块化好,再使用 webpack
webpack功能非常丰富,我们这里只涉及到最常用的一部分
安装
安装 webpack工具库
首先当然要安装 webpack 工具库,执行如下命令
npm install webpack webpack-cli --save-dev
安装 JSX编译库
然后,安装 webpack 构建过程中需要的其它工具库
我们这里的示例 主要是要转化JSX, 当然要安装 转化工具, 这里我们选择 Babel这个工具
执行如下命令
npm install --save-dev @babel/core @babel/cli @babel/preset-react
要让 Babel 有效集成到 webpack 的构建过程,还需要安装 babel-loader 库
执行如下命令
npm install --save-dev babel-loader
也可以选择esbuild,
// 确保先安装 esbuild
npm install --save-dev esbuild
// 再安装 针对webpack 的esbuild loader库
npm install --save-dev esbuild-loader
开发环境
运行webpack时,它根据配置文件执行不同的功能
在开发调试时 运行 webpack, 是为了 调试、测试 我们的前端代码,
所以需要webpack 提供本地的web服务器,并且为调试程序提供便利
安装 webpack-dev-server
webpack-dev-server 是一个开发环境下的 web服务,
作用类似 vscode live server 扩展
执行如下命令 安装
npm install --save-dev webpack-dev-server
配置文件
先做开发调试环境的 webpack 配置文件。
配置文件就是一个js文件,放在项目根目录下,
文件名可以自己定义,比如 wpcfg.run_dev_server.js
示例配置如下
视频讲解里面的 12:50 处对proxy配置的讲解是老版本的 现在版本的配置有了一些改变,以下面示例中的写法为准
var path = require('path');
module.exports = {
// 指明是开发模式
mode : 'development',
// 入口模块定义
entry: {
hyms: 'hyms.js',
},
module: {
// 模块处理规则
rules: [
{
test: /\.jsx$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: [ '@babel/preset-react']
}
},
]
},
// 模块搜索路径
resolve: {
modules: [
path.resolve('./'),
path.resolve('./node_modules')
],
},
// 一种包含源码的编译结果,方便调试对应到源代码
devtool: 'inline-source-map',
// web服务配置
devServer: {
//端口
port: 80,
// 指定 非webpack产生的 其他文件的根目录,
// 比如 html、一些公共的库、图片资源,
static:{
// __dirname 代表当前项目目录
directory: __dirname,
} ,
// 指定 本dev服务可以代理的请求的url 和 被代理的服务地址
// 通常是代理 后端API服务的请求
proxy: [
{
context: ['/api'],
target: 'http://localhost:8234',
},
],
},
};
启动
有两种方式可以 启动 webpack开发服务
-
执行命令
在命令行窗进入到项目根目录下,执行命令
webpack serve -c wpcfg.run_dev_server.js --progress -
vscode前端项目任务菜单
可以在
package.json文件中,scripts配置项中,添加如下配置"scripts": { "run-dev-server": "webpack serve --config wpcfg.run_dev_server.js --progress" },上面的示例中:
run-dev-server是任务名,后面的
webpack serve --config wpcfg.run_dev_server.js --progress是该任务要执行的命令
推荐使用方法2启动
启动服务后,修改任意一个jsx,发现更新可以立即重新编译加载。
注意 : 运行webpack js程序的,是 node.js 而不是浏览器。
生产环境
我们开发好的项目代码,最终要部署到生产环境上
那么生产环境大体是怎样的呢?
这就必须先了解一下系统的大体架构
前后端分离的架构下,系统架构大体如下图所示
我们下面就是讲解其中前端部分的部署
Nginx 配置
首先看 web服务、反向代理 软件
生产环境是 不应该 使用 webpack dev server 作为 web 服务的
Nginx 是比较常用的 web服务/反向代理 软件
生产环境下,Nginx基本都是安装在Linux环境下,需要大家熟悉Linux
大家,
可以 点击这里,学习 Ubuntu Linux
可以 点击这里,学习 Nginx在Ubuntu上的安装
使用Nginx的关键是配置好配置文件
下面是一个 Nginx 配置文件的片段, 包含了通常要修改的部分
http {
# 后端服务 (比如Gunicorn/Django)
# 主要配置 名称(这里是 apiserver) 地址和端口
upstream apiserver {
# 和后端服务 最多 20 个 空闲连接
keepalive 20;
# 后端服务地址和端口
server 127.0.0.1:8234;
}
# 配置 HTTP 服务器信息
server {
# 网站的域名,这里请改为你申请的域名, 如果没有域名,使用IP地址。
server_name www.yoursite.com;
# 网站的端口
listen 80;
# 配置动态数据请求怎么处理
# 下面这个配置项说明了,当 HTTP 请求 URL以 /api/ 开头,
# 则转发给 apiserver 后端服务去处理
location /api/ {
proxy_pass http://apiserver;
proxy_redirect off;
proxy_set_header Host $host;
proxy_http_version 1.1;
add_header Access-Control-Allow-Origin *;
}
# 访问静态文件的根目录,通常为html、css、js、图片资源等
location / {
root websites/bysms;
}
}
所以,我们前端开发的结果文件,包括 js、css、图片资源文件 ,应该上传放在 location / -> root 指定的根目录下面
本例中,就是 websites/bysms 目录
那么我们应该产生怎样的前端结果文件,放到web服务目录下呢?
首先,所有的库文件(比如react),应该使用production版,而不是development版。
因为 production版 体积更小 并且 执行性能更高
webpack配置
另外我们自己的代码 可以用 webpack 构建出生产环境的代码文件
为了方便大家直观的查看构建结果,推荐安装 webpack-bundle-analyzer 库
执行 npm i webpack-bundle-analyzer --save-dev 命令安装即可
然后为生产环境代码构建创建一个webpack配置文件,文件名可以叫 wpcfg.build_prod.js
配置示例如下
var path = require('path');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
// 生产环境模式
mode : 'production',
// 入口模块
entry: {
hyms:'hyms.js'
},
// 输出结果设置
output: {
// __dirname是node.js 的 内置变量, 代表当前目录
path: 'd:/tools/nginx-1.20.2/websites/bysms/',
filename: '[name].js'
},
// 优化设置
optimization: {
// 代码分割
splitChunks: {
chunks: 'all',
automaticNameDelimiter: '-',
},
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: [ '@babel/preset-react']
}
},
]
},
resolve: {
modules: [
path.resolve('./'),
path.resolve('./node_modules')
],
},
plugins: [
new BundleAnalyzerPlugin({analyzerMode:'static'})
]
};
执行构建
有两种方式可以 执行 webpack 构建任务
-
执行命令
在命令行窗进入到项目根目录下,执行命令
webpack --config wpcfg.build_prod.js -
vscode前端项目任务菜单
可以在
package.json文件中,scripts配置项中,添加如下配置"scripts": { "build-product": "webpack --config wpcfg.build_prod.js" },
执行后,可以发现产生 一个结果文件hyms.js 。
打开 hyms.js 发现其内容还被最小化了。
通过 webpack-bundle-analyzer 产生的report页面显示: hyms里面包含了所有的其它import的依赖模块
大家就可以把这个结果文件和其它不需要webpack构建的文件,比如css、图片、库目录等,放到 web服务的静态文件目录下
静态导入和动态导入
静态导入
webpack 根据代码中的 import 分析 模块之间的依赖关系。
最简单的就是静态导入,比如
import {Medicine} from "./medicine.jsx"
import {Customer} from "./customer.jsx"
import Order from "./order.jsx"
webpack 就会把这些模块处理好后和当前模块打包在一个Bundle中
动态导入-文件名确定
如果是 动态 import 比如
if(condition)
const { default: App } = await import('./customer.js');
也能识别出来。
并且 动态 import 的 模块 不和 bundle 在一个chunk中。
动态导入-文件名为变量
但是如果import里面的是变量,比如
let hash = window.location.hash
const URL2MODULE = {
'' : './customer.js',
'customer' : './customer.js',
'medicine' : './medicine.js',
'order' : './order.js',
}
hash = hash.slice(1)
let modulePath = URL2MODULE[hash]
if (!modulePath)
modulePath = URL2MODULE['']
// 导入的是变量
const { default: App } = await import(modulePath);
那就不行了。
因为 await import(modulePath) 中 modulePath 是变量,
要运行时才能确定内容,webpack 没有办法知道导入的到底是什么。
那怎么办呢?
可以参考官方文档 https://webpack.js.org/api/module-methods/#dynamic-expressions-in-import
方法是:加上 路径前缀 限制搜索范围,而且要 有 神奇注释 指明,如下
let hash = window.location.hash
const URL2MODULE = {
'' : 'customer.jsx', // 去掉前缀./
'customer' : 'customer.jsx', // 去掉前缀./
'medicine' : 'medicine.jsx', // 去掉前缀./
'order' : 'order.jsx', // 去掉前缀./
}
hash = hash.slice(1)
let modulePath = URL2MODULE[hash]
if (!modulePath)
modulePath = URL2MODULE['']
// let App = modulePath
const { default: App } = await import(
/* webpackInclude: /.*(customer|medicine|order)\.jsx/ */
/* webpackExclude: /.*pracs.+/ */
`./${modulePath}`); // 加上前缀./ , 这样限制搜索范围,否则webpack放弃搜索
要特别注意: webpackInclude 和 webpackExclude 神奇注释 里面正则表达式匹配的是 整个路径, 而不是 只有变量那部分。
运行发现产生的动态加载的 bundles名称类似: 93.js、600.js、397.js
那程序运行时,await import('./customer.jsx') 是怎么找到对应的js文件和代码的呢?
原来,这种情况,webpack会修改入口bundle代码 (可以打开hyms.js看看),
会产生一些额外的辅助代码,指定原来的动态导入文件 和 新bundle文件之间的对应关系,
如下所示
n={
"./customer.jsx":[93,93],
"./medicine.jsx":[600,600],
"./order.jsx":[397,397]
}
当程序动态加载 ./customer.jsx 时, 后面代码会查询到对应的其实是 93.js ,转化为实际导入 93.js
其它
去除重复包含模块 - 暂时解决不了以后再说
通过webpack-bundle-analyzer 发现,每个
93.js、600.js、397.js 都包含了同一个文件 crud.js
package.json添加命令
package.json中添加 run-dev-server 和 build-product
"scripts": {
"run-dev-server": "webpack serve --config wpcfg.run_dev_server.js --progress",
"build-product": "webpack --config wpcfg.build_prod.js"
},
使用 esbuild-loader
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: 'esbuild-loader',
exclude: /node_modules/,
options: {
loader:'jsx',
target: 'es2015'
}
},
]