主题
设置标题和站标与使用 sme-router 构建单页面应用(项目初始化 2/2)
本页对应 课堂视频-project,共 7 段视频,约 1 小时 30 分。
学习目标
- 能够为生产模式配置
postcss-loader与autoprefixer,并通过构建产物验证目标 CSS 属性已按兼容性要求添加浏览器私有前缀。 - 能够安装并配置
ejs-loader,在views目录创建 EJS 模板,并在路由模块中导入、调用模板函数、传入数据后渲染到指定挂载节点。 - 能够解释 EJS 模板导入后的函数返回值及渲染机制,并独立完成首页、登录页、管理员列表页、广告列表页等模板的路由接入与页面展示验证。
- 能够区分 webpack 开发模式与生产模式配置,独立完成 CSS 压缩配置,并验证 CSS 与 JS 在生产构建后均被压缩且互不覆盖。
- 能够克隆并启动后端项目,使用 API 测试工具按接口文档完成管理员添加与列表查询请求,并验证前端可获取后端返回数据。
- 能够依据功能需求文档分析后台管理系统的登录、管理员增删改查、广告管理、图片上传与权限控制等模块,并说明单页面应用中路由切换与标题站标设置的基本实现方式。
知识点
先理解 public/index.html 的角色
为什么需要:这一节要解决两个“门面”问题:浏览器标签页上显示什么标题,以及标签页左侧显示什么站标(favicon)。后台管理系统打包后,最终入口仍是 dist/index.html,所以标题和站标必须在模板阶段配置好。
怎么想:public/index.html 不是普通静态页,而是 HtmlWebpackPlugin 的模板。标题不要写死,而是从插件配置里取;站标是静态资源,放在 public 根目录,由复制插件带到 dist。转写稿里反复提到的“舞台”,就是模板中最终承载页面内容的挂载点;模板内容会进入这个舞台,再生成最终的 dist/index.html。
怎么做:先把模板写成下面这样,保留标题变量和站标引用:
html
<!DOCTYPE html>
<html lang="zh-CN">
<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><%= htmlWebpackPlugin.options.title %></title>
<link rel="icon" href="./favicon.ico">
</head>
<body>
<h1>Hello Shirly</h1>
</body>
</html>易错点:title 里的变量名必须和 HtmlWebpackPlugin 配置中的自定义选项一致;favicon.ico 要真实放在 public 根目录,且 href 路径要和复制后的位置一致。转写稿强调“路径保持一致”,这里就是 ./favicon.ico 对应 dist/favicon.ico。
用 HtmlWebpackPlugin 注入标题
为什么需要:把标题写成配置项,后续改项目名、环境名时只改 webpack 配置,不用反复改模板;这也是工程化里“模板与数据分离”的思路。
怎么想:HtmlWebpackPlugin 实例上的 title 会成为模板中的 htmlWebpackPlugin.options.title。模板用 EJS 语法 <%= %> 输出即可。转写稿里讲模板引擎时说过,模板就是做替换:把特殊符号替换成指定数据。这里替换的数据就是标题。
怎么做:在 config/webpack.base.config.js 的 HtmlWebpackPlugin 中写 title: '尚硅谷广告管理系统 shirly'。完整配置如下:
js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
// 入口
entry: path.resolve(__dirname, '../src/app.js'),
// 出口
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'js/[name].[hash:12].js',
clean: true,
publicPath: '/'
},
// 插件
plugins: [
// 打包 html 资源
new HtmlWebpackPlugin({
// 起始html文件
template: path.resolve(__dirname, '../public/index.html'),
// 将 script 放在 body 结尾
inject: 'body',
// 压缩优化
minify: {
removeAttributeQuotes: true, // 去除引号
removeComments: true, // 去除注释
//collapseWhitespace: true, // 代码折叠
},
// 自定义的选项
title: '尚硅谷广告管理系统 shirly'
}),
// 将源目录(public)中的文件复制到目标目录(项目输出目录)
new CopyWebpackPlugin({
patterns: [
{
// 源目录
from: path.resolve(__dirname, '../public'),
// 目标目录
to: path.resolve(__dirname, '../dist'),
globOptions: {
ignore: ['**/**.html'] // 忽略所有的html文件
}
},
],
}),
]
}易错点:title 要写在 new HtmlWebpackPlugin({...}) 内部;模板变量拼写不能错;改完 webpack 配置后要重启 devServer,转写稿里就出现过“加了 loader 但没重新运行导致解析错误”的调试过程。
安装并配置 CopyWebpackPlugin 复制站标
为什么需要:favicon.ico 不是 JS/CSS 模块,webpack 不会自动把它当依赖打包进 dist。如果不复制,dist/index.html 里的 <link rel="icon" href="./favicon.ico"> 会 404,浏览器标签页就没有站标。
怎么想:把 public 里的静态资源整体复制到 dist,但要忽略 public/index.html。因为最终 index.html 要由 HtmlWebpackPlugin 生成,不能被原样复制覆盖。转写稿里也明确说:public 里的 CSS、JS 等文件会被复制插件复制,路径保持一致;这里 favicon 同理。
怎么做:先安装插件:
shell
npm install copy-webpack-plugin -D然后按上面的完整配置加入 CopyWebpackPlugin,关键是 from 指向 public,to 指向 dist,并用 globOptions.ignore 忽略所有 html。
易错点:patterns 是数组;ignore 写成 ['**/**.html'],不要误伤 favicon.ico;from/to 用 path.resolve 保证路径稳定;不要复制 public/index.html 覆盖生成文件。转写稿提到 devServer 会把内容打到内存里,所以开发时看不到 dist 文件是正常的;要看复制结果,执行生产构建后检查 dist。
构建验证与调试
为什么需要:配置写完不等于生效,必须确认标题变量被替换、favicon 被复制到正确位置。
怎么想:先跑构建,检查 dist/index.html 的 <title> 是否变成“尚硅谷广告管理系统 shirly”,再检查 dist/favicon.ico 是否存在;启动开发服务器后看浏览器标签页和 Network 面板中 favicon 是否 200。转写稿里说“改配置要重新运行”,这里同样适用。
怎么做:执行构建,打开 dist/index.html 或启动 devServer;若 favicon 仍不显示,先查 dist 根目录有没有文件,再查 href 是否写错,最后强刷浏览器排除缓存。
易错点:clean: true 会清空 dist,每次构建后要重新确认复制结果;favicon 浏览器缓存很顽固,调试时用强刷;标题不生效先看模板变量和 title 选项是否对应;站标 404 先看复制插件是否忽略了 ico。
安装 sme-router
为什么需要:本节要构建单页面应用。转写稿里提到,单页面应用的特点是“浏览器从不刷新”,切换内容时是“把原来的删掉,换成新的”,再根据 Ajax 请求数据更新视图。要让不同 URL 对应不同视图,就需要前端路由。sme-router 就是本节使用的路由库。
怎么想:先把它作为项目依赖安装,再在入口文件里创建路由实例。安装是工程化项目使用第三方库的第一步,不能直接把库代码抄进项目。
怎么做:
shell
npm install sme-router易错点:安装命令要在项目根目录执行;安装后要确认依赖写入 package.json,再继续写入口代码。
创建路由实例与默认 hash 模式
为什么需要:路由实例负责监听地址变化,并把匹配到的视图渲染到页面容器中。笔记中 new SMERouter('app') 的 'app' 对应页面里的容器,路由最终要把内容放进这个“舞台”。
怎么想:先明确默认模式。笔记说明默认是 hash 模式,地址形如:
text
http://adv.manage.fuming.site/#/index/advlisthash 模式的特点是 URL 中带 #。转写稿反复强调单页面应用“浏览器不刷新”,所以这里不是每换一个地址就重新请求整页,而是由前端路由接管视图切换。
怎么做:在 src/app.js 中导入并创建实例:
js
// 导入
import SMERouter from 'sme-router'
// 创建实例
// 默认是 hash 模式,特点 http://adv.manage.fuming.site/#/index/advlist
// 可以改为 history 模式,特点 http://adv.manage.fuming.site/index/advlist
const router = new SMERouter('app')易错点:new SMERouter('app') 里的 'app' 要和页面容器 id 对应;如果容器不存在,路由渲染就没有落点。还要分清 hash 模式地址带 #,history 模式地址不带 #。
注册路由与兜底重定向
为什么需要:单页面应用有多个视图,例如首页、管理员列表、广告列表、登录。每个地址要告诉路由“匹配到谁,就渲染什么”。同时用户可能输入不存在的地址,需要兜底,避免出现空白页。
怎么想:先注册具体路由,再写通配符 *。从笔记代码看,res.render('首页') 是把对应视图内容交给路由容器;res.redirect('/index') 是在未知路径时跳回首页。转写稿里说“所有内容都要展示到 APP 里面”,可以理解为路由容器就是最终承载视图的舞台。
怎么做:
js
router.route('/index', (req, res) => {
res.render('首页');
});
router.route('/admin', (req, res) => {
res.render('管理员列表');
});
router.route('/adv', (req, res) => {
res.render('广告列表');
});
router.route('/login', (req, res) => {
res.render('登录');
});
router.route('*', (req, res) => {
res.redirect('/index');
})易错点:路由路径要写清楚,例如 /index 不要漏掉前面的 /;* 兜底规则应放在具体路由之后,避免先匹配到通配符;res.render 和 res.redirect 的用途不同,前者渲染视图,后者跳转地址。
允许使用 history 模式
为什么需要:hash 模式地址带 #,不够直观;history 模式地址形如:
text
http://adv.manage.fuming.site/index/advlist但 history 模式在开发环境下有一个问题:直接访问或刷新 /index/advlist 这类前端路由地址时,开发服务器可能把它当成真实文件路径,从而返回 404。因此需要让 devServer 支持前端路由回退。
怎么想:笔记给出的是在 config/webpack.dev.config.js 中开启 historyApiFallback: true。它的作用是让开发服务器在找不到对应静态资源时回退到入口页面,再由前端路由接管地址。转写稿里也提到“改了 webpack 配置,要重新运行”,所以改完配置不能只刷新页面,要重启开发服务。
怎么做:
js
const {merge} = require('webpack-merge');
const baseConfig = require('./webpack.base.config');
module.exports = merge(baseConfig, {
// 模式
mode: 'development',
// 自动开启服务
devServer: {
host: 'shirly.com',
port: '80',
open: true,
// 开发中,允许前端路由使用 history 模式
historyApiFallback: true
}
})易错点:只有使用 history 模式时才需要关注 historyApiFallback;hash 模式地址带 #,不依赖这个回退配置。修改 webpack.dev.config.js 后要重启 devServer,否则新配置可能不生效。还要注意 devServer 的 host、port 要与本地访问地址一致。
为什么需要 ejs 模板
路由切换时,如果每个页面都手写字符串,页面一复杂就难维护。模板的作用是把 HTML 里的特殊标记替换成指定数据,再把结果字符串交给路由渲染到 #app 舞台中。转写里反复强调:这里的 EJS 模板不是完整 HTML,不需要 html/head/body,只写要插入舞台的局部内容。
Webpack 默认能处理 JS 等模块,但 import './views/index.ejs' 这种扩展名它不认识,所以必须加 loader。加上 ejs-loader 后,导入的模板会变成一个函数;调用函数并传入数据,返回值才是替换好的模板字符串。
安装 ejs-loader
shell
npm install ejs-loader -D-D 表示开发依赖:loader 是给 Webpack 构建用的。易错点:装完忘记改配置,或改完配置不重启 dev server。转写中先遇到 parse failed,重启 start 后才正常。
配置 config/webpack.base.config.js
js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
// 入口
entry: path.resolve(__dirname, '../src/app.js'),
// 出口
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'js/[name].[hash:12].js',
clean: true,
publicPath: '/'
},
// loaders
module: {
rules: [
{
test: /\.ejs$/,
loader: 'ejs-loader',
options: {
variable: 'data',
//interpolate : '\\{\\{(.+?)\\}\\}',
//evaluate : '\\[\\[(.+?)\\]\\]'
}
}
]
},
// 插件
plugins: [
// 打包 html 资源
new HtmlWebpackPlugin({
// 起始html文件
template: path.resolve(__dirname, '../public/index.html'),
// 将 script 放在 body 结尾
inject: 'body',
// 压缩优化
minify: {
removeAttributeQuotes: true, // 去除引号
removeComments: true, // 去除注释
//collapseWhitespace: true, // 代码折叠
},
// 自定义的选项
title: '尚硅谷广告管理系统 shirly'
}),
// 将源目录(public)中的文件复制到目标目录(项目输出目录)
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, '../public'),
to: path.resolve(__dirname, '../dist'),
globOptions: {
ignore: ['**/**.html']
}
},
],
}),
]
}怎么想:test: /\.ejs$/ 锁定模板文件;loader: 'ejs-loader' 做转换;variable: 'data' 决定模板里接收数据的变量名。易错点:只用一个 loader 可写 loader,多个 loader 才常用 use 数组;interpolate/evaluate 是改定界符的,转写中选择注释掉,继续用默认 <%= %>、<% %>。
创建局部模板文件
创建 views/index.ejs、views/admin.ejs、views/login.ejs、views/adv.ejs(笔记列表里的 .js 按转写与下方 import 应为 .ejs)。先写局部内容,例如 index.ejs 里写 <h1>hello</h1>,再测试数据:<%= data.title %>、<%= data.message %>。
易错点:不要写完整 HTML;模板最终插入 public/index.html 的 #app。模板里引用的 CSS/JS 放到 public,由 CopyWebpackPlugin 复制到 dist,路径与 public/index.html 中保持一致。
在 src/app.js 中引入并使用
js
// 导入
import SMERouter from 'sme-router'
// 导入一个模板
import indexV from './views/index.ejs';
import adminV from './views/admin.ejs';
import loginV from './views/login.ejs';
import advV from './views/adv.ejs';
// 创建实例
// 默认是 hash 模式,特点 http://adv.manage.fuming.site/#/index/advlist
// 可以改为 history 模式,需要第二个参数设置为 html5 特点 http://adv.manage.fuming.site/index/advlist
const router = new SMERouter('app', 'html5')
router.route('/index', (req, res) => {
//console.log(indexV);
res.render(indexV({title:'Shirly', message:'锄禾日当午'}));
});
router.route('/admin', (req, res) => {
res.render(adminV());
});
router.route('/adv', (req, res) => {
res.render(advV());
});
router.route('/login', (req, res) => {
res.render(loginV());
});
router.route('*', (req, res) => {
res.redirect('/index');
})怎么想:import indexV 拿到的是函数;indexV({...}) 才返回模板字符串;res.render(字符串) 把内容放进舞台。转写中先 console.log(indexV) 看到函数,再 console.log(indexV()) 看到模板内容,这就是推导过程。易错点:直接 res.render(indexV) 传了函数;忘记调用;模板里写 data.title 但传参名字不对;以为 console.log 输出在终端,实际运行在浏览器,应看浏览器控制台。验证时访问 /index、/admin、/adv、/login,* 会重定向到 /index。
课堂画面






代码示例
1. 设置标题和站标(public/index.html + config/webpack.base.config.js)
html
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<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">
<!-- 标题由 HtmlWebpackPlugin 的自定义选项注入 -->
<title><%= htmlWebpackPlugin.options.title %></title>
<!-- 站标,配合 CopyWebpackPlugin 从 public 复制到 dist -->
<link rel="icon" href="./favicon.ico">
</head>
<body>
<!-- 单页面应用的“舞台”,sme-router 会把模板渲染到这里 -->
<div id="app"></div>
</body>
</html>js
// config/webpack.base.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
// 入口
entry: path.resolve(__dirname, '../src/app.js'),
// 出口
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'js/[name].[hash:12].js',
clean: true,
publicPath: '/'
},
// 插件
plugins: [
// 打包 html 资源
new HtmlWebpackPlugin({
// 起始 html 文件
template: path.resolve(__dirname, '../public/index.html'),
// 将 script 放在 body 结尾
inject: 'body',
// 压缩优化
minify: {
removeAttributeQuotes: true, // 去除引号
removeComments: true, // 去除注释
//collapseWhitespace: true, // 代码折叠
},
// 自定义的选项
title: '尚硅谷广告管理系统 shirly'
}),
// 将源目录(public)中的文件复制到项目输出目录
new CopyWebpackPlugin({
patterns: [
{
// 源目录
from: path.resolve(__dirname, '../public'),
// 目标目录
to: path.resolve(__dirname, '../dist'),
globOptions: {
ignore: ['**/**.html'] // 忽略所有的 html 文件
}
},
],
}),
]
}关键行与易错点
<%= htmlWebpackPlugin.options.title %>里的htmlWebpackPlugin是插件注入的模板变量,options.title对应配置中自定义的title字段,两处名字必须一致,否则标题变成undefined。CopyWebpackPlugin必须ignore: ['**/**.html'],否则public/index.html会被原样复制到dist,覆盖掉HtmlWebpackPlugin生成并自动注入<script>的成品页面。- 安装时用
npm install copy-webpack-plugin -D,-D表示开发依赖;转写稿强调 loader/插件装成开发依赖即可。 favicon.ico用的是相对路径./favicon.ico,因为复制后与index.html同级,不要写成public/xxx。- 改了 webpack 配置文件后,正在运行的 dev server 必须重启,否则报 “Module parse failed” 之类的解析错误(转写稿中加完 ejs-loader 后就踩过这个坑)。
2. 用 sme-router + ejs 模板构建单页面应用(src/app.js + views/index.ejs)
ejs
<!-- src/views/index.ejs -->
<h1><%= data.title %></h1>
<p><%= data.message %></p>js
// src/app.js
// 导入路由
import SMERouter from 'sme-router'
// 导入模板(ejs-loader 把模板编译成一个函数)
import indexV from './views/index.ejs';
import adminV from './views/admin.ejs';
import loginV from './views/login.ejs';
import advV from './views/adv.ejs';
// 创建实例
// 默认是 hash 模式:http://adv.manage.fuming.site/#/index/advlist
// 第二个参数设为 'html5' 即 history 模式:http://adv.manage.fuming.site/index/advlist
const router = new SMERouter('app', 'html5')
router.route('/index', (req, res) => {
// indexV 是函数,调用后返回替换好数据的字符串
res.render(indexV({ title: 'Shirly', message: '锄禾日当午' }));
});
router.route('/admin', (req, res) => {
res.render(adminV());
});
router.route('/adv', (req, res) => {
res.render(advV());
});
router.route('/login', (req, res) => {
res.render(loginV());
});
router.route('*', (req, res) => {
res.redirect('/index');
})js
// config/webpack.base.config.js —— 与第 1 段相同的文件,仅补充 module.rules
module: {
rules: [
{
test: /\.ejs$/,
loader: 'ejs-loader',
options: {
variable: 'data', // 模板中数据的变量名
//interpolate : '\\{\\{(.+?)\\}\\}',
//evaluate : '\\[\\[(.+?)\\]\\]'
}
}
]
}js
// config/webpack.dev.config.js —— 允许前端路由使用 history 模式
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');
module.exports = merge(baseConfig, {
mode: 'development',
devServer: {
host: 'shirly.com',
port: '80',
open: true,
// 开发中,允许前端路由使用 history 模式
historyApiFallback: true
}
})关键行与易错点
new SMERouter('app', 'html5')的第一个参数'app'必须与index.html里<div id="app">的 id 一致,这就是“舞台”,所有res.render()的内容都塞进它里面。- 用
'html5'模式时,devServer 必须开historyApiFallback: true,否则在/index直接刷新会 404;hash 模式则不需要。 import indexV from './views/index.ejs'默认只认.js,.ejs是“不寻常的扩展名”,必须先配ejs-loader,否则报解析错误。indexV导出的是函数,res.render(indexV)传函数进去会渲染不出内容,必须indexV({...})调用取返回值(字符串)。- 模板里用
data.xxx取数据,是因为 loader 配置了variable: 'data';把配置改成别的名字,模板里的变量名也要跟着改。 variable对应的对象是整体赋给模板的,可以传数组、函数,用法和之前学的 ejs 完全一样。- 模板文件只写“舞台内部”的片段,不要写
<!DOCTYPE html>、<head>这些,它们属于public/index.html。 - 页面用到的 css/js/图片统一放进
public,由CopyWebpackPlugin复制进dist,在public/index.html里引入一次即可。
3. 生产模式:CSS 兼容前缀与压缩(postcss.config.js / .browserslistrc / config/webpack.prod.config.js)
js
// postcss.config.js(项目根目录)
module.exports = {
plugins: [
// autoprefixer 已随 postcss-loader 一起安装
require('autoprefixer')
]
}# .browserslistrc(项目根目录)
# 全球使用率超过 99.5% 的浏览器都要兼容
cover 99.5%js
// config/webpack.prod.config.js
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = merge(baseConfig, {
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
// 顺序从右到左:postcss-loader 要在 css-loader 之前处理
use: ['style-loader', 'css-loader', 'postcss-loader']
},
// 如果是 less,postcss-loader 写在 css-loader 之后、less-loader 之前
// use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader']
]
},
// 与 plugins 平级的一级配置项
optimization: {
minimizer: [
`...`, // 保留默认的 JS 压缩插件
new CssMinimizerPlugin() // 再加上 CSS 压缩
]
}
})关键行与易错点
- loader 执行顺序从右到左:
postcss-loader必须放在css-loader之后(数组里更靠右),因为要在 CSS 进入打包流程前先补私有前缀;less 项目则放在css-loader和less-loader之间。 - 安装时是“两个包”:
postcss-loader(负责接入)和autoprefixer(真正加前缀),转写稿强调别只装一个。 - 光有 loader 不够,根目录还必须有
postcss.config.js(引入 autoprefixer)和.browserslistrc(告诉它兼容到什么程度),缺一个前缀都不会生成。 - 前缀不是所有属性都会加:比如
filter: blur()在支持的浏览器上本来就不需要私有前缀,所以“没看到-moz-”不等于配置失败。 optimization.minimizer与plugins平级,不要误写进plugins数组。- 一旦手写
minimizer,就会覆盖 webpack 内置的 JS 压缩,所以必须加`...`保留默认值,否则出现“CSS 压缩了、JS 没压缩”的现象(转写稿里正是这个坑)。 - 生产环境才做抽离、兼容、压缩;开发模式为了打包快可以不做,配置分开写(
webpack.dev.config.js/webpack.prod.config.js)用webpack-merge合并基础配置。
常见错误
改完 webpack 配置,页面报
Module parse failed: Unexpected token或行为完全没变- 现象:装完 loader、写好 rules 保存,刷新浏览器仍是老样子;控制台报解析错误,提示不认识
.ejs/ 新语法。 - 原因:devServer 启动时已把配置读进内存,配置文件的改动不会被热更新感知;改的是
webpack.base.config.js,跑的还是旧编译结果。 - 修法:停掉 devServer 重新
npm start。养成习惯——凡改config/*.js、.browserslistrc、postcss.config.js,一律重启;只有src下的业务代码才靠热更新。
- 现象:装完 loader、写好 rules 保存,刷新浏览器仍是老样子;控制台报解析错误,提示不认识
postcss-loader 装了、配了,产物里却一个
-webkit-都没多- 现象:
dist里 CSS 和源文件一模一样,无前缀;构建过程不报任何错。 - 原因:三个坑之一——只装了
postcss-loader没装autoprefixer;或项目根目录缺postcss.config.js;或缺.browserslistrc,autoprefixer 没有目标浏览器就"什么都不做"。 - 修法:按 loader →
postcss.config.js(plugins: { autoprefixer: {} })→.browserslistrc三件套补齐。补完先确认一件事:不是所有属性都该有前缀,像filter在目标浏览器版本里本来就无需前缀,没加前缀不等于没生效——要换一个确实需要前缀的属性(如user-select、backdrop-filter)来验证。
- 现象:
postcss-loader 位置放错,前缀没生效或 less 直接编译失败
- 现象:纯 CSS 场景能加前缀,一换成 less 就报错;或反过来,less 正常但前缀丢失。
- 原因:loader 数组是从右往左执行的,
postcss-loader必须在css-loader之前跑(即数组里写在css-loader右边),而less-loader还要更靠右。 - 修法:统一按
['style-loader', 'css-loader', 'postcss-loader', 'less-loader']排序;MiniCssExtractPlugin.loader替换style-loader时同理,位置不动。
加了 CSS 压缩插件后,JS 反而不压缩了(或反之)
- 现象:production 打包,CSS 压成一行了,JS 却还是带换行和注释;把 CSS 插件注释掉,JS 又自动压缩了。
- 原因:
optimization.minimizer是整体覆盖,不是追加。手写minimizer: [new CssMinimizerPlugin()]把内置的 terser 默认值顶掉了。 - 修法:写成
minimizer: ['...', new CssMinimizerPlugin()],用'...'保留默认项。验收方式:打包后同时打开dist/js/*.js和dist/css/*.css,两个都应是单行。
模板渲染出来是
function ...源码或一片空白- 现象:
res.render(indexV)之后页面显示函数体文本;或者模板里data.title位置是空的。 - 原因:
import进来的.ejs是一个函数,不调用就只是个函数对象;调用时不传参,模板里的data就是undefined。 - 修法:
res.render(indexV({ title: 'xx' }))——先调用拿字符串,再交给 render。模板里形参名默认是data,由ejs-loader的options.variable决定,改名要两边同步。
- 现象:
dist/index.html里的<title>变成<%= htmlWebpackPlugin.options.title %>原文,或 favicon 404- 现象:打包后打开页面,标签页标题就是一串模板语法;图标不显示。
- 原因:
CopyWebpackPlugin把public整个拷了过去,连index.html一起覆盖了HtmlWebpackPlugin编译产物;favicon 同理,源目录路径和产物路径没对上。 - 修法:
globOptions.ignore: ['**/*.html'],让 html 只由HtmlWebpackPlugin产出。favicon 用./favicon.ico这类相对路径引用,并确认它在dist根目录下。
history 模式路由,点链接正常,一刷新就 404
- 现象:
/adv直接敲地址栏或 F5 白屏/404,从/index点过去却正常。 - 原因:两处漏配之一——
SMERouter实例没传'html5'仍走 hash;或 devServer 没开historyApiFallback,服务器把/adv当真实路径去找文件。 - 修法:
new SMERouter('app', 'html5')+devServer.historyApiFallback: true同时到位。注意这只解决开发环境,生产环境要靠服务端 fallback 配置。
- 现象:
练习
标题与站标闭环验证 修改
HtmlWebpackPlugin的title为"XX 广告管理系统",同时保证浏览器标签页标题正确、favicon 正常显示。分别在npm start和npm run build后验证一遍,并说明:为什么dist/index.html里不能出现未被替换的<%= %>?写出你为防止它出现所做的配置。browserslist 与 autoprefixer 的因果验证 在
.browserslistrc中分别写入last 2 versions和cover 99.5%,用同一个需要前缀的属性(建议user-select: none或backdrop-filter: blur(2px))打包两次,对比dist中 CSS 的差异。回答:两次产物前缀有什么不同?为什么"没加前缀"有时是正确的?压缩冲突修复 先按错误写法只写
minimizer: [new CssMinimizerPlugin()]打包一次,记录 JS 是否被压缩;再改成'...'版本打包,对比两次产物。用一句话解释'...'在这个数组里代表什么,以及为什么它必须写。带数据的模板渲染 为
/adv路由准备一个views/adv.ejs,路由中传入一个数组(如 3 条广告{id, title, img}),模板里用forEach渲染成列表。完成后回答两个问题:(a) 模板里为什么可以直接用data这个名字?(b) 若把ejs-loader的variable改成locals,需要改动哪些地方?兜底路由与刷新验证 把
router.route('*')从redirect('/index')改成渲染一个自定义的views/404.ejs。然后分别测试:从/index点链接跳到不存在的路径、直接在地址栏输入不存在的路径并回车、在该路径上按 F5。记录三种情况下的表现,并解释差异来源。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 生产模式-css兼容 | 10:18 |
| 2 | 使用 ejs 模板 | 33:35 |
| 3 | 生产模式-压缩css | 14:08 |
| 4 | webpack总结 | 02:04 |
| 5 | 项目介绍 | 16:29 |
| 6 | 后端项目启动 | 09:20 |
| 7 | 项目功能分析 | 04:29 |
小结
- 配置类文件(webpack config、postcss config、browserslist)改动后必须重启 devServer,这是排查"改了没反应"的第一动作,不是第二动作。
- loader 数组是执行顺序的倒序,
postcss-loader的插入位置由css-loader和less-loader夹出来——位置错了不报错,只是静默失效,比报错更难查。 - autoprefixer 是"三件套"缺一不可:loader、
postcss.config.js、目标浏览器声明。少了 browserslist,工具会以为你没有兼容目标而什么都不做。 - 前缀加不加由目标浏览器决定,不由"这个属性看起来新不新"决定。判断兼容性处理是否生效,要拿确实需要前缀的属性做实验,而不是盯着某个属性看。
optimization.minimizer是覆盖语义,'...'是保留默认值的开关;这条规律同样适用于其他"整体覆盖型"配置项,遇到"配了 A 结果 B 失效"先往这个方向想。- 模板文件导入得到的是函数,调用才得到字符串;
res.render收的是字符串。传不传参决定了模板里的数据变量是否有值。 public是"原样搬运"的静态资源区,不参与模块依赖分析;src里 import 的东西才走 loader。把两者搞混,就会出现资源 404 或 html 被覆盖的问题。- 报错先读关键字:
Module parse failed指向缺 loader,Cannot find module指向路径或依赖没装,404在 history 模式下优先怀疑 fallback。定位方向对了,修起来都是一两行的事。