Skip to content

项目初始化配置(项目初始化 1/2) ​

本页对应 课堂视频-project,共 7 段视频,约 2 小时 27 分。

学习目标 ​

  • 能使用 npm 初始化项目并安装 webpack、webpack-cli、webpack-dev-server、html-webpack-plugin、webpack-merge 等依赖,搭建基础工程目录与入口文件。
  • 能拆分 webpack 基础配置、开发配置和生产配置,使用 webpack-merge 合并配置,并在 package.json 中配置 start/build 脚本,实现开发服务自动启动、热更新与生产打包。
  • 能通过 HtmlWebpackPlugin 设置页面标题,通过 CopyWebpackPlugin 复制站点图标和静态资源,并处理插件冲突与忽略规则。
  • 能使用 sem-router 构建单页面应用,配置 hash 模式路由和页面容器,实现不同 URL 展示不同视图。
  • 能切换 sem-router 到 history 模式,并在 webpack-dev-server 中配置 historyApiFallback,验证开发环境下前端路由回退。
  • 能在生产模式使用 mini-css-extract-plugin 及配套 loader 从 JS 中抽离 CSS,生成独立 CSS 文件并完成打包验证。

知识点 ​

一、项目初始化与依赖安装 ​

为什么需要:前端工程也要跑在 Node 环境上,webpack、loader、plugin 都依赖 Node;先初始化才能形成 package.json,后续安装和脚本配置才有落点。
怎么想:先装最小闭环:webpack 本体、命令行工具、开发服务器、HTML 插件、配置合并工具。转写稿明确说 CSS 相关先不装,因为页面样式先用现成模板,不纳入模块体系。
怎么做:

bash
npm init -y
npm i webpack webpack-cli webpack-dev-server html-webpack-plugin webpack-merge -D

易错点:这些是开发依赖,要加 -D;不要第一个命令就装十几个包;注意 webpack 拼写,转写稿中多次出现口误式拼写,落地时以正确包名为准。

二、目录结构与入口出口 ​

为什么需要:工程化项目要模块化组织资源,不能把页面、入口、配置混在根目录。
怎么想:仿 Vue/React 的结构:src 放入口,public 放模板和静态资源,config 放 webpack 配置,dist 是输出目录。
怎么做:基础配置里写入口、出口和清理输出:

js
entry: path.resolve(__dirname, '../src/app.js'),
output: {
  path: path.resolve(__dirname, '../dist'),
  filename: 'main.[hash:12].js',
  clean: true,
  publicPath: '/'
}

易错点:配置文件移入 config 后,入口不能再写 ./src/app.js,要用 path.resolve(__dirname, '../src/app.js');filename 是 JS 文件名,不是 HTML;publicPath: '/' 会让资源以网站根目录请求,直接双击 dist/index.html 打不开是正常现象。

三、拆分 base/dev/prod 与合并 ​

为什么需要:开发模式要速度和 devServer,生产模式要压缩、抽离、优化;公共配置不能重复写两遍。
怎么想:公共配置放 webpack.base.conf.js,开发配置和生产配置分别引入 base,再用 webpack-merge 合并;后面的配置覆盖前面的同名项。
怎么做:

js
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.conf')
module.exports = merge(baseConfig, { mode: 'development' })

package.json 中配置:

json
"scripts": {
  "start": "webpack serve --config ./config/webpack.dev.conf.js",
  "build": "webpack --config ./config/webpack.prod.conf.js"
}

易错点:如果自己再写 plugins,可能覆盖 base 里的 plugins,所以要用 merge;配置文件改动不会热更新,必须重启命令。转写稿中曾出现 module not found,最后定位为 HtmlWebpackPlugin 的 template 路径忘了随 config 目录调整,修正后才正常启动和打包。

四、开发服务自动化 ​

为什么需要:每改一次代码就重新打包很累;开发时需要自动打包、自动刷新、自动打开浏览器。
怎么想:webpack-dev-server 会临时启动静态资源服务器,把打包结果放内存,不生成 dist,所以热更新快。
怎么做:在开发配置中加:

js
devServer: {
  host: 'localhost',
  port: 8080,
  open: true,
  historyApiFallback: true
}

运行 npm start。
易错点:命令要带 serve,只写 webpack 是普通打包;devServer 不产 dist;historyApiFallback 只是开发阶段临时配置,正式部署还要服务器配合。

五、标题、站标与 copy 插件冲突 ​

为什么需要:后台管理系统要设置标题和站点图标;现成 CSS/JS 已经压缩好,不需要再经过 loader 处理,直接复制更快。
怎么想:标题可通过 HtmlWebpackPlugin 的模板变量写入;站标等静态文件用 copy-webpack-plugin 原样复制。
怎么做:

bash
npm i copy-webpack-plugin -D

配置中从 public 复制到 dist,并忽略 index.html:

js
new CopyWebpackPlugin({
  patterns: [{ from: path.resolve(__dirname, '../public'), to: path.resolve(__dirname, '../dist'), globOptions: { ignore: ['**/index.html'] } }]
})

易错点:HtmlWebpackPlugin 已处理 index.html,copy 又复制它就会冲突;copy 不会额外处理文件,目录结构会保持;适合复制现成资源,不适合替代需要编译的模块。

六、单页面应用与路由模式 ​

为什么需要:后台管理系统页面不刷新,只切换模块,能减少请求、提升体验。
怎么想:用一个 HTML 舞台,URL 变化由前端路由控制内容替换。hash 模式带 #,不会让后端识别井号后的路径;history 模式去掉 #,更像普通 URL,但刷新时需要服务器 fallback。
怎么做:安装 sme-router(转写稿强调它是产品依赖,不加 -D);页面留 <div id="app"></div>;路由写法类似 Express:

js
router.route('/', (req, res) => res.render('首页'))
router.route('/admin', (req, res) => res.render('管理员列表'))
router.route('*', (req, res) => res.redirect('/index'))

开发阶段在 devServer 配 historyApiFallback: true。
易错点:history 模式刷新会向后端请求路径,正式部署必须由服务器配置;devServer 的配置只是临时开发用;hash 模式不需要后端识别井号后内容。

七、生产模式抽离 CSS ​

为什么需要:开发时 CSS 可注入 JS,生产环境希望 CSS 和 JS 分离,便于浏览器加载和缓存。
怎么想:开发用 style-loader 把样式注入页面;生产用 mini-css-extract-plugin 抽成独立 CSS 文件并压缩。
怎么做:

bash
npm i mini-css-extract-plugin -D

生产配置中加插件,并把 style-loader 换成 MiniCssExtractPlugin.loader,css-loader 保留。
易错点:插件和 loader 都要配;plugins 覆盖问题仍要用 merge 解决;配置改动要重启;小图片可能被转成 base64 写进 CSS,这是正常现象。

本节准备工作的目标 ​

本节笔记里的两个地址先原样保留:

text
前端演示地址:http://adv.manage.fuming.site/
后端代码仓库地址:https://gitee.com/sh230320/backend_project

本节笔记未附图片,下面保留原有链接与命令,并按转写稿补充“为什么需要 → 怎么想 → 怎么做 → 易错点”的推导。

初始化项目与安装依赖 ​

为什么需要:前端工程也要用 Node 工具链,webpack、loader、plugin 都依赖 Node,所以第一步是生成 package.json。

怎么想:先不要一次装十几个包,按当前阶段能想到的依赖装:webpack 本体与 CLI、开发服务器、HTML 处理插件、配置合并工具。CSS 相关先不装,因为页面样式先用现成模板,不纳入模块体系。

怎么做:

bash
npm init -y
npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin webpack-merge

易错点:转写稿里反复纠正“webpack”不是“wi pack”;webpack-cli 和 webpack 要一起装,否则命令行跑不起来。

建立目录与入口文件 ​

为什么需要:webpack 需要一个入口,HTML 需要一个模板,后面还要把源码和静态资源分开。

怎么想:仿照常见前端结构,源码放 src,页面模板放 public。先建 src/app.js 作为入口,建 public/index.html 作为单页面应用的“舞台”。

怎么做:在 src/app.js 先写一点内容,在 public/index.html 写一个 #app 容器。转写稿中先用 hello 验证链路是否通。

易错点:入口文件名不要随手写 index.js 后又忘了配置;模板文件路径在配置文件移动后最容易忘改。

写基础 webpack 配置 ​

为什么需要:只有把入口、出口、HTML 插件纳入 webpack 管理,才能打包和启动服务。

怎么想:先写公共配置 config/webpack.base.config.js,入口用 path.resolve(__dirname, '../src/app.js'),出口用 path.resolve(__dirname, '../dist'),并配置 filename、clean、publicPath。

怎么做:核心配置可简化为:

js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: path.resolve(__dirname, '../src/app.js'),
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'main.[hash:12].js',
    clean: true,
    publicPath: '/'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../public/index.html'),
      inject: 'body'
    })
  ]
}

易错点:转写稿中先临时运行 npx webpack --config config/webpack.base.config.js 验证;若报“模板文件不存在”,不是文件真没了,而是配置文件移到 config 后 template 的相对路径没改。

开发服务自动化 ​

为什么需要:每改一次代码都手动打包太累,开发阶段需要自动编译、自动刷新。

怎么想:webpack-dev-server 会启动临时静态资源服务器,把打包结果放内存,不生成 dist,所以第一次打开慢,热更新快。

怎么做:在开发配置中写:

js
devServer: {
  host: '127.0.0.1',
  port: 8080,
  open: true
}

启动命令要带 serve,或写进 scripts:

json
{
  "scripts": {
    "start": "webpack serve --config config/webpack.dev.config.js",
    "build": "webpack --config config/webpack.prod.config.js"
  }
}

易错点:只运行 webpack 不会启动服务;devServer 配置改了也必须重启,因为它不能热更新自己。devServer 不生成 dist,要打包必须用 build。

设置标题和站标 ​

为什么需要:页面标题和 favicon 是后台系统的基本信息,且标题可以在打包 HTML 时注入。

怎么想:用 HtmlWebpackPlugin 的自定义选项传 title,模板里用 EJS 语法读取;站标用 copy-webpack-plugin 把 public 中现成资源复制到 dist。

怎么做:安装:

bash
npm i -D copy-webpack-plugin

配置复制时忽略 HTML,避免和 HTML 插件冲突:

js
new CopyWebpackPlugin({
  patterns: [
    {
      from: path.resolve(__dirname, '../public'),
      to: path.resolve(__dirname, '../dist'),
      globOptions: { ignore: ['**/*.html'] }
    }
  ]
})

易错点:转写稿中复制 public 全部内容时报“冲突”,原因是 HTML 插件也要处理 index.html;忽略所有 HTML 即可。copy-webpack-plugin 不额外处理文件,目录结构保持不变,适合现成 CSS、JS、图片。

拆分开发/生产配置与单页面路由准备 ​

为什么需要:开发模式强调速度,生产模式强调压缩、抽离、兼容性;公共配置不能重复写两遍。

怎么想:用 webpack-merge 合并 base、dev、prod。开发配置加 mode: 'development' 和 devServer;生产配置加 mode: 'production'。

怎么做:

bash
npm i -D webpack-merge
js
const { merge } = require('webpack-merge')
const base = require('./webpack.base.config')

module.exports = merge(base, {
  mode: 'development',
  devServer: { host: '127.0.0.1', port: 8080, open: true }
})

路由用 sem-router 构建单页面应用,安装时不加 -D,因为打包后仍需要路由。哈希模式默认带 #;history 模式要去掉 #,开发时在 devServer 配 historyApiFallback: true。

易错点:转写稿强调,historyApiFallback 只是开发阶段临时让 webpack-dev-server 回退到首页,正式部署还要由 Nginx 等静态服务器配置,不能把开发服务器当生产服务器。路由页面最终都渲染到 public/index.html 的 #app 容器里,所以模板里必须有这个容器。

生产模式抽离 CSS ​

为什么需要:开发时 CSS 可以通过 style-loader 塞进 JS,生产环境希望 CSS 单独成文件,便于浏览器缓存和并行加载。

怎么想:用 mini-css-extract-plugin 同时替代 style-loader,并添加插件生成 CSS 文件。

怎么做:

bash
npm i -D mini-css-extract-plugin

在生产配置中加插件,并在 module.rules 中用它的 loader 处理 CSS。

易错点:只加插件不加 loader 不够;mini-css-extract-plugin 的 loader 负责从 JS 中抽离 CSS,插件负责生成文件,两者要配合。生产配置若直接写 plugins,可能覆盖基础配置里的 HTML 插件,所以要用 webpack-merge 合并,而不是手写对象展开。

初始化:先把工程立起来 ​

为什么需要:这个项目虽然不是 Node 后端项目,但 webpack、loader、插件、开发服务器都运行在 Node 环境里,所以要先有 package.json 来记录项目信息、依赖和脚本。
怎么想:先初始化,再安装工具,最后才写配置;不要一上来就堆配置文件。
怎么做:在项目根目录终端执行:

shell
npm init

一路回车即可,生成 package.json。
易错点:npm init 只是初始化,不会自动安装 webpack;项目根目录不要选错,否则后面路径全乱。

安装依赖:分清工具职责 ​

为什么需要:webpack 负责打包,webpack-cli 让我们能在命令行调用 webpack,webpack-dev-server 提供开发服务器和热更新,html-webpack-plugin 处理 HTML 模板并自动注入 JS,webpack-merge 用来合并公共配置和模式配置。
怎么想:这些目前都属于开发阶段工具,所以统一加 -D 安装到开发依赖。
怎么做:

shell
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin webpack-merge -D

易错点:不要漏掉 webpack-cli,否则命令行无法正常执行 webpack;webpack-merge 不是 webpack 本体,它是配置合并工具。

建立目录:入口、模板、输出要分清 ​

为什么需要:webpack 需要知道从哪里开始打包、HTML 模板在哪里、最终输出到哪里。
怎么想:按常见工程结构,业务代码放 src,页面模板放 public,配置放 config,输出目录由 webpack 生成。
怎么做:创建 src/app.js、public/index.html,再创建 config/webpack.base.config.js。
易错点:配置文件放进 config 后,所有相对路径都要以配置文件所在目录为基准重新计算,不能想当然写 ./src。

基础配置:入口、出口、HTML 插件 ​

为什么需要:基础配置放开发和生产都要用的部分,避免重复代码。
怎么想:入口用 path.resolve(__dirname, '../src/app.js'),出口用 path.resolve(__dirname, '../dist');HTML 插件指定模板,并把 script 注入到 body 结尾。
怎么做:

js
const path = require('path');
const HtmlWebpackPlugin = require('html-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: [
        new HtmlWebpackPlugin({
            // 起始html文件 
            template: path.resolve(__dirname, '../public/index.html'),
            // 将 script 放在 body 结尾
            inject: 'body',
            // 压缩优化
            minify: {
                removeAttributeQuotes: true,  // 去除引号
                removeComments: true, // 去除注释
                collapseWhitespace: true, // 代码折叠
            }
        })
    ]
}

易错点:template 路径最容易写错;publicPath: '/' 表示资源以网站根目录为基准,本地双击 dist/index.html 可能反而打不开,这是正常的。clean: true 会在打包时清理输出目录,不要手动改 dist 里的生成文件。

开发与生产配置:用 merge 合并 ​

为什么需要:开发模式要 devServer、自动打开浏览器;生产模式只要打包优化,不需要开发服务器。
怎么想:公共部分放 base,各自差异放 dev/prod,再用 webpack-merge 合并。
怎么做:

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
    }
})
js
const {merge} = require('webpack-merge');
const baseConfig = require('./webpack.base.config');

module.exports = merge(baseConfig, {

    // 模式
    mode: 'production'
})

易错点:host: 'shirly.com' 需要本机 hosts 能解析到本机,否则浏览器打不开;端口 80 可省略端口号,但可能受权限或占用影响。开发服务器是把资源放在内存里,不会生成 dist;要生成 dist 必须执行生产打包。

配置 scripts:把长命令收起来 ​

为什么需要:每次手写 npx webpack server --config ... 太长,容易拼错。
怎么想:把开发启动和生产打包分别写进 scripts,以后只记 npm start 和 npm run build。
怎么做:

json
{
  "name": "frontend_project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "npx webpack server --config ./config/webpack.dev.config.js",
    "build": "npx webpack --config ./config/webpack.prod.config.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "html-webpack-plugin": "^5.5.3",
    "webpack": "^5.87.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1",
    "webpack-merge": "^5.9.0"
  }
}

运行:

shell
npm start   # 启动
npm run build  # 打包

易错点:start 可以省略 run,build 不能省略;配置文件不在根目录且不叫默认名时,必须用 --config 指定。修改 webpack 配置文件后,devServer 不会热更新配置本身,需要重新启动命令。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

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: [
    new HtmlWebpackPlugin({
      // 起始 html 文件
      template: path.resolve(__dirname, '../public/index.html'),
      // 将 script 放在 body 结尾
      inject: 'body',
      // 自定义选项,模板中可用 <%= htmlWebpackPlugin.options.title %> 读取
      title: '上硅谷广告管理系统',
      // 压缩优化
      minify: {
        removeAttributeQuotes: true, // 去除引号
        removeComments: true,        // 去除注释
        collapseWhitespace: true,    // 代码折叠
      }
    }),

    // 原目录 → 目标目录,原样复制不额外处理
    new CopyWebpackPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, '../public'),   // 原目录
          to: path.resolve(__dirname, '../dist'),       // 目标目录
          globOptions: {
            ignore: ['**/index.html']                   // 忽略所有 html,交给 HtmlWebpackPlugin
          }
        }
      ]
    })
  ]
};

关键行与易错点

  • entry / output.path / template 全部用 path.resolve(__dirname, '../xxx'):配置文件从项目根目录挪进了 config/,写 ./src 会以命令行所在目录(项目根)为基准,反而找不到;用 __dirname + ../ 才是“以配置文件所在目录为基准”,这也是笔记里强调“入口出口都要改一改”的原因。
  • filename: 'js/[name].[hash:12].js' 配的是 JS 输出文件名,不是 HTML;[name] 默认取 main。
  • publicPath: '/' 让打包后的 <script src="/js/main.xxx.js"> 以斜杠开头,即按站点根目录请求。代价是 dist 必须整体作为网站根目录,双击 index.html 直接打开会失效——这是刻意为之,不是 bug。
  • clean: true 每次打包先清空 dist,所以 dist 里的东西永远不要手动改,改了下一次打包就没了。
  • 最大坑:HtmlWebpackPlugin 已经在处理 public/index.html,CopyWebpackPlugin 又要把 public 下所有东西复制到 dist,两者对同一个文件抢着写,会直接报 Conflict(冲突)错误。必须用 globOptions.ignore 忽略 html('**/index.html' 表示任意层级下的 index.html)。
  • 用 Copy 而不是 html-loader 的原因:public 里是别人写好的、已压缩的 CSS/JS/图片,不需要再过一遍 loader,原样复制既快又不会破坏目录结构,也避免与 HTML 模板语法冲突。

js
// config/webpack.dev.config.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
  }
});
js
// config/webpack.prod.config.js
const path = require('path');
const { merge } = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const baseConfig = require('./webpack.base.config');

module.exports = merge(baseConfig, {
  // 模式
  mode: 'production',

  // 生产模式把 css 从 js 中抽离成单独文件
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },

  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[hash:12].css'
    })
  ]
});
json
// package.json(节选)
{
  "scripts": {
    "start": "npx webpack server --config ./config/webpack.dev.config.js",
    "build": "npx webpack --config ./config/webpack.prod.config.js"
  }
}

关键行与易错点

  • 用 webpack-merge 的 merge(base, 自己的) 而不是 {...base, ...自己}:展开语法是浅合并,自己再写一个 plugins 会把基础配置里的 HtmlWebpackPlugin 整个覆盖掉,HtmlWebpackPlugin 就“消失”了;merge 是深合并,plugins 数组会追加。
  • 合并顺序决定优先级:后写的覆盖先写的,所以 mode、devServer 这类差异配置必须写在第二个参数里。
  • devServer 是 mode、plugins 平级的一级配置,不是 loader 也不是 plugin;port: 80 就是线上地址不带端口号的原因(80 是 HTTP 默认端口,可省略)。
  • npm start 里的 webpack server 一个字母都不能少:只写 npx webpack --config ... 是打包,不会起服务、不会热更新、不会有 open。
  • webpack-dev-server 只把产物放进内存,不生成 dist 目录;想看 dist 就用 npm run build。
  • 易错点:改了 webpack.*.config.js 本身不会触发热更新——它监听的是 src、public 这些目录,监听不了自己,必须手动重启 npm start。
  • 生产配置里 MiniCssExtractPlugin 需要“插件 + loader”双管齐下:插件负责创建 css 文件,MiniCssExtractPlugin.loader 负责替换掉开发时的 style-loader(原来是在 JS 里动态创建 <style>,现在是抽离写入文件)。只装插件不换 loader,css 还是不会出来。

html
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- 从插件自定义选项中取值 -->
  <title><%= htmlWebpackPlugin.options.title %></title>
  <!-- 站标随 CopyWebpackPlugin 一起复制到 dist 根目录,路径保持不变 -->
  <link rel="icon" href="/favicon.ico">
</head>
<body>
  <!-- 单页面应用的“舞台” -->
  <div id="app"></div>
</body>
</html>
js
// src/app.js
import SemRouter from 'sem-router';

// 第一个参数是舞台容器的 id,不加 #
const router = new SemRouter('app');

// 不同 URL 进入页面的不同模块,页面本身不刷新
router.get('/', (req, res) => res.render('首页'));
router.get('/admin', (req, res) => res.render('管理员列表'));
router.get('/adv', (req, res) => res.render('广告列表'));
router.get('/login', (req, res) => res.render('登录'));

// 兜底:匹配不到统一重定向回首页
router.get('*', (req, res) => res.redirect('/'));

关键行与易错点

  • <%= htmlWebpackPlugin.options.title %> 是 EJS 模板语法,由 HtmlWebpackPlugin 在打包 html 时做一次替换;变量名 htmlWebpackPlugin 是插件自动注入的(小驼峰命名),和你在 JS 里 require('html-webpack-plugin') 取的变量名无关。
  • <div id="app"></div> 与 new SemRouter('app') 必须对上,且不能写成 #app。忘了这一步,路由配得再对页面也不会变,只会一直显示原来写死的“hello shirly”。
  • 单页面应用的本质:始终只有 index.html 这一个页面,URL 变化时由 JS 请求数据、替换 #app 内的内容,浏览器不刷新、不重新请求页面。
  • 默认是 hash 模式(地址带 #),hash 部分不会发给后端,所以不需要后端配合;改成 history 模式(无 #)需要给构造器传第二个参数,并且开发阶段要在 devServer 里开 historyApiFallback: true。
  • 易错点:historyApiFallback 只是 webpack-dev-server 这个临时开发服务器的一条配置,正式部署时换成 Nginx 等专业静态服务器,必须由运维另写配置(如 try_files),不能指望开发服务器的配置带到线上。
  • router.get('*', ...) 的兜底一定要放在最后,路由是按顺序匹配的,写在前面会把所有正常路由都吃掉。

常见错误 ​

  1. 现象:执行 npm start 后提示找不到配置文件或启动失败。
    原因:配置文件已移入 config/,但仍按默认根目录 webpack.config.js 查找,或 scripts 中 --config 路径写错。
    修法:在 package.json 的 scripts 中显式指定 --config ./config/webpack.dev.config.js / webpack.prod.config.js,并核对文件名大小写与相对路径。

  2. 现象:打包报错 Module not found,提示入口、输出目录或 HTML 模板不存在。
    原因:配置文件放入 config/ 后,entry、output.path、HtmlWebpackPlugin.template 仍使用相对项目根目录的旧路径。
    修法:统一改为基于 __dirname 的 path.resolve(__dirname, '../src/app.js')、../dist、../public/index.html,逐项检查所有路径型配置。

  3. 现象:copy-webpack-plugin 复制 public 时打包失败,报冲突。
    原因:html-webpack-plugin 已处理 public/index.html,copy-webpack-plugin 又复制整个 public,两者争抢同一输出文件。
    修法:在 copy-webpack-plugin 的 globOptions.ignore 中忽略 HTML 模板,如忽略 **/*.html 或至少忽略 index.html。

  4. 现象:开发模式启动后找不到 dist 目录,误以为打包失败。
    原因:webpack-dev-server 将产物放在内存中,不写入磁盘;只有 webpack 构建命令才输出 dist。
    修法:开发调试看浏览器服务地址即可;需要检查产物时执行 npm run build,不要用 npm start 的结果判断是否生成 dist。

  5. 现象:生产配置中新增插件后,基础配置里的 HtmlWebpackPlugin 等插件“消失”或 HTML 不再生成。
    原因:直接展开基础配置后又覆盖 plugins 数组,导致原插件被整体替换。
    修法:使用 webpack-merge 合并配置;或手工合并 plugins,确保新增插件追加而不是覆盖。

  6. 现象:CSS 抽离配置后样式未生成独立文件,或构建报 loader 相关错误。
    原因:只加了 MiniCssExtractPlugin 插件,未把 style-loader 替换为 MiniCssExtractPlugin.loader;或规则被后续配置覆盖。
    修法:同时配置插件与 module.rules,在生产规则中使用 MiniCssExtractPlugin.loader 处理 CSS,并确认规则合并顺序。

  7. 现象:修改 webpack 配置文件后页面没有自动更新。
    原因:devServer 监听的是源码/公共资源目录,不能热更新自身配置文件。
    修法:修改 webpack.*.config.js 后手动停止并重新执行 npm start。

  8. 现象:使用 history 路由后,开发环境刷新子路由 404;部署后同样 404。
    原因:开发环境未开启 historyApiFallback;生产环境未让静态服务器回退到 index.html。
    修法:开发配置中设置 devServer.historyApiFallback: true;生产部署时由 Nginx 等服务器配置 fallback 到 index.html,不能把 devServer 配置当生产方案。

练习 ​

  1. 路径迁移排查:把 webpack.base.config.js 放入 config/ 后,故意保留一个旧的相对路径,运行 npm run build,根据报错定位并修复 entry、output.path、template 三类路径问题。
    验收:构建成功,dist/index.html 正确引入 JS。

  2. 插件冲突修复:在基础配置中同时启用 HtmlWebpackPlugin 与 CopyWebpackPlugin 复制 public,复现冲突后通过 globOptions.ignore 修复。
    验收:public 下的图标等静态资源被复制,HTML 模板不重复输出。

  3. 开发/生产行为对比:分别执行 npm start 与 npm run build,观察 dist 是否生成、浏览器访问地址是否带端口、产物是否在内存中。
    验收:能说明两种命令的产物位置差异,并提交对比记录。

  4. CSS 抽离配置:在生产配置中接入 MiniCssExtractPlugin,将 style-loader 替换为抽离 loader,构建后检查是否生成独立 CSS 文件及 source map。
    验收:dist 中存在独立 CSS 文件,页面样式正常。

  5. history 路由验证:将路由切换为 history 模式,在开发配置中开启 historyApiFallback,访问子路由并刷新。
    验收:开发环境刷新不 404;同时写出生产部署需服务器 fallback 的说明。

原视频对照 ​

#视频时长
1项目初始化配置33:43
2开发服务自动化16:29
3设置标题和站标22:53
4开发生成两个配置文件23:16
5使用sem-router构建单页面应用01-hash模式26:08
6生产模式-抽离css16:58
7使用sem-router构建单页面应用01-history模式07:49

小结 ​

  1. 配置文件移入子目录后,所有路径型配置都必须重新以配置文件位置为基准校正。
  2. webpack-dev-server 面向开发调试,产物在内存中;webpack 构建才面向 dist 产物。
  3. 多环境配置优先使用 webpack-merge,避免手工展开导致 plugins、module.rules 被覆盖。
  4. HtmlWebpackPlugin 与 CopyWebpackPlugin 同时处理 HTML 时必须显式忽略模板文件。
  5. 静态资源是否需要进入模块体系要区分:已压缩、无需处理的第三方资源适合直接复制。
  6. 生产模式应关注 CSS 抽离、压缩与兼容性;开发模式应优先保证启动和热更新速度。
  7. history 路由的 fallback 在开发环境靠 devServer,生产环境必须由实际静态服务器配置解决。
  8. 修改 webpack 配置文件不会触发热更新,必须重启开发服务。