普通视图

发现新文章,点击刷新页面。
昨天以前首页

webpack + Travis CI 自动部署项目应用

作者 anran758
2020年6月8日 12:43

我们知道 Github Pages 是 Github 免费提供给用户展示页面的一项服务。当我们完成项目开发后,想将页面部署到 Github Pages 时,该要怎么操作呢?

可以在 GitHub 的储存库设置中设置用于展示页面的分支,该分支只保留构建后的静态资源,也就是源码与编译后的静态资源分离。按照传统的做法是:手动运行编译命令,编译后再复制到指定分支中。这样操作很繁琐,但使用 Travis CI 持续集成服务之后就可以不用操心这些事了。

概念

既然我们要使用 Travis CI,首先得搞清楚人家具体是干嘛的吧?

Travis CI 是一个 **持续集成(Continuous integration, CI)**。它与 git 相耦合,每当有 commit 提交时,它将自动触发构建与测试。若运行结果符合预期,才将新代码集成到 主流(mainline) 中,这样使应用更加健壮。

值得注意的是,Travis CI 提倡每次 commit 都是独立较小的改动,而不是突然提交一大堆代码。因为这有助于后续构建失败时可以回退到正常的版本。

运行构建时,Travis CI 将 GitHub 存储库克隆到全新的虚拟环境中,并执行一系列任务来构建和测试代码。如果这些任务中的一项或多项失败,则将构建视为已损坏。如果所有任务均未失败,则认为构建已通过,Travis CI 会将代码部署到 Web 服务器或应用程序主机中(在本文中是指 Github Pages 服务)。

准备

在使用之前,需要准备一个 Github 的账号对 Travis CI 进行授权。

  1. 接着通过 Github 的账号登录 Travis CI,点击 SIGN IN WITH GITHUB
  2. 点击后会被重定向到 Github 进行授权。
  3. 授权后,若是第一次登录的话会被重定向至引导页:
  4. 点击引导页第一步的按钮,使用 GitHub Apps 激活储存库。可以选择给全部储存库都激活,也可以激活指定储存库。本文以 <username>.github.io 为例:

    注意: 这个 username 是你自己的 Github 用户名。笔者的 usernameanran758 那储存库的名字就为 **anran758.github.io**。

  5. 激活后会被重定向到设置页,点击待部署的储存库右侧的 setting 按钮,跳转至 Travis CI 储存库设置页。我们需要在此页设置部署 Github Pages 时所需的环境变量:

环境变量的值需要从 Github 拿拥有部署权限的 token:

  1. 打开 Github,点击头像,再点击 Settings 进入设置页:
  2. 进入设置页面后在左侧边栏点击开发者设置:
  3. 跳转后在左侧边栏点击 Personal access tokens, 然后在头部点击 Generate new token:
  4. 填写 token 备注、权限,最后点击生成 token:
  5. 生成 token 后点击复制按钮,复制到粘贴板: 注意要妥善保管好 token,重新刷新页面后这个 token 将不会再展示出来。如果忘记了 token 的话,也只能在 token 编辑页中重新生成。这会导致所有用到该 token 的应用都要更新值。 比方说有三个应用使用了该 token,重新生成后只在一个应用更新的值,那其他两个应用不更新就无法使用了。
  6. 复制 token 后切回 Travis CI 储存库的设置页,添加环境变量:

这样我们的准备工作就完成的差不多了。

配置

在项目目录中新建文件 .travis.yml,内容如下:

/.travis.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
language: node_js
node_js:
- lts/*

install:
- yarn install # npm ci
script:
- yarn test # npm run test
- yarn build # npm run build

deploy:
provider: pages
local_dir: dist
target_branch: master
on:
branch: develop
token: $GITHUB_TOKEN
skip_cleanup: true
keep_history: true
committer_from_gh: true

由于 webpack 项目依赖 Node.js,因此语言(language) 设置为 node_js,同时还指定使用最新的 LTS Node.js 版本(lts/*)。

install 是安装部署所需的依赖项,script 则是用于运行测试或构建脚本。他们都是 Travis 的工作生命周期(Job Lifecycle)必触发的钩子(阶段)。

install 钩子若有脚本/命令运行失败的话,整个构建会停止。而 script 钩子表现则不同,当有脚本/命令运行失败后虽然构建会失败,但还会继续执行后面的脚本。如 yarn test 运行失败后会继续跑 yarn build 命令。

以下是 Travis CI 主要的阶段流程图:

graph TDA[before_install] --> B[install]A & B -.-> Z((停止构建))B --> C[before_script]C --> D[scrip]D --> E(after_success)D --> F(after_failure)E & F --> G[before_deploy]G --> H[deploy]H --> I[after_deploy]I --> J[after_script]

部署

通过 deploy 可以指定部署方式,下面将逐个介绍部署所用的选项:

provider 是部署类型。现在我们想将页面部署到 Github Pages,那就需要将 provider 设为 pages

local_dir 指定要推送到 Github Pages 的目录,默认为当前目录。webpack 默认的输出目录是 /dist,因此需要将值设为 dist。除此之外,Travis CI 默认情况下会删除构建期间创建的所有文件,因此需要设置 skip_cleanup: true 保留构建出来的 dist 目录.

on.branch 有 commit 提交的话,Travis CI 将从 on.branch 分支运行编译脚本,编译后会把 local_dir 目录强制推送到 target_branch 中。(target_branch 默认值为 gh-pages)

现在我们要部署的储存库是 <username>.github.io。这种类型的储存库有些特殊——它只能在 master 分支展示构建后的代码,而不能修改为其他分支。在 GitHub 储存库的 Settings 中的 Source 选项可以看到详细信息:

然而其他储存库则没有这种限制:

因此要部署到 <username>.github.io 储存库的话,target_branch 只能设为 master,触发编译的 on.branch 分支则可以自己定义。

其他储存库可以按照标准流程来开发:

  • develop 作为开发分支
  • master 作为主分支
  • gh-pages 作为页面展示分支

等功能开发并测试完毕后,将 develop 的代码合并到 master 分支并推送至远程。Traivis CI 检测到 matsercommit 提交后会自动运行脚本构建,构建完毕后将输出目录推送至 gh-pages 分支。

当然 Github Pages 也不是随便来一个人就可以部署的,你想要部署到储存库中首先得有该储存库的操作权限吧?token 就是证明你身份的东西。在上文中我们预先设置好了一个名为 GITHUB_TOKEN 的环境变量,此处我们可以通过 $GITHUB_TOKEN 直接取出该环境变量的值即可。

其他还有一些细节问题可以调整:比如推送构建后的代码到 target_branch 时使用的是强制推送(git push --force),如果你觉得这种强制覆盖历史记录的方式有点暴力的话,可以设置 keep_history: true 来保留提交记录。

自动部署后 commit 提交者默认是 Travis CI 的信息。也可以设置 committer_from_gh 允许 Travs CI 使用令牌所有者的个人信息来提交 commit

配置完毕后现在只需将 .travis.yml 提交到远程,Travis CI 就开始工作了:

甚至还可以在 Github commit 信息中看到编译的情况:

如果构建出问题的话,Travis CI 还会发邮件提示你:

部署成功后就可以直接通过浏览器访问啦~ 储存库部署的是 <username>.github.io 的话,访问链接为 https://<username>.github.io/。其他储存库可以访问 https://<username>.github.io/<repoName>

比如笔者的主页与博客是两个项目分离的,部署后的链接地址为 https://anran758.github.iohttps://anran758.github.io/blog

Travis CI CLI

还可以通过 Travis CI CLI 来进行操作:

按照文档的 Installation 部分安装 Travis CI CLI

安装完毕后通过命令行进入储存库目录,输入 travis -v 来检查是否安装成功。

Travis CI 有两个不同域名版本的 API,一个是 .com 新版本,.org 是旧版本的。先确定自己使用的是哪个平台,再设定它:

1
2
3
4
5
6
7
# 默认是 .org
travis endpoint
# API endpoint: https://api.travis-ci.org/

# 笔者使用的是 .com 的平台,因此需要修改默认的模式。设置 `--com` 和 `--pro` 的效果是相等的。
travis endpoint --com --set-default
# API endpoint: https://api.travis-ci.com/ (stored as default)

确定版本后输入 travis logintravis login --pro 进行登录。Mac os 系统可能会遇到 Travis Ci CLI 依赖的 ruby 版本和系统自带 ruby 有冲突:

1
2
3
4
5
6
7
8
9
10
11
travis login --com
# We need your GitHub login to identify you.
# This information will not be sent to Travis CI, only to api.github.com.
# The password will not be displayed.

# Try running with --github-token or --auto if you don't want to enter your password anyway.

# Username: anran758
# Password for anran758: ***********
# Unknown error
# for a full error report, run travis report --pro

若不想处理这些麻烦的依赖问题,可以在 Travis CI 的个人设置页 复制 access_token~/.travis/config.yml 的配置中:

1
2
3
4
# code ~/.travis/config.yml # 通过 vscode 进行修改

# 通过 vim 进行修改
vim ~/.travis/config.yml

修改 endpoints 下的 access_token 并保存后,在命令输入 travis accounts --pro 检查是否成功:

1
2
3
4
travis accounts --pro
# travis accounts --pro
# anran758 (Anran758): not subscribed, 18 repositories
# To set up a subscription, please visit travis-ci.com.

这样就登录完毕啦~ 接着在输入 travis logs 就可以查看日志:

1
2
3
4
5
# 查看最新构建的日志
travis logs

# 查看指定构建日志
travis logs 2

还可以清空指定构建的日志:

1
2
# travis logs 2 --d # -d 简短选项
travis logs 2 --delete

参考资料:

从零构建 webpack 脚手架(基础篇)

作者 anran758
2020年5月4日 22:58

webpack 是一个现代 JavaScript 应用程序的静态模块打包工具,它对于前端工程师来说可谓是如雷贯耳,基本上现在的大型应用都是通过 webpack 进行构建的。

webpack 具有高度可配置性,它拥有非常丰富的配置。在过去一段时间内曾有人将熟练配置 webpack 的人称呼为 “webapck 工程师”。当然,这称呼只是个玩笑话,但也能从侧面了解到 webpack 配置的灵活与复杂。

为了能够熟练掌握 webpack 的使用,接下来通过几个例子循序渐进的学习如何使用 webpack。

以下 Demo 都可以在 Github 的 webpack-example 中找到对应的示例,欢迎 star~

起步

webpack@v4.0.0 开始,就可以不用再引入配置文件来打包项目。若没有提供配置的话,webpack 将按照默认规则进行打包。默认情况下 src/index 是项目的源代码入口,打包后的代码会输出到 dist/main.js 上。

首先来初始化一个项目,项目名为 getting-started

1
2
3
4
5
6
7
8
# 创建项目文件夹
mkdir getting-started

# 进入项目目录
cd getting-started

# npm 项目
npm init -y

初始化项目后,项目目录会新增一个 package.json,该文件记录了项目依赖的相关信息。若想要使用 webpack 的话需要安装它的依赖: webpack (本体)和 webpack-cli (可以在命令行操作 webpack 的工具):

1
2
3
4
5
6
# -D 和 --save-dev 选项都可以用于安装开发依赖
# npm i --save-dev webpack webpack-cli
npm i -D webpack webpack-cli

# 或者使用 yarn 安装开发依赖
yarn add -D webpack webpack-cli

接着创建 webpack 所需的默认入口文件 src/index.js 以及测试模块所用的 src/log.js 文件。此时的项目结构大致如下:

1
2
3
4
5
6
  .
├── package.json
+ ├── src
+ │ ├── index.js
+ │ └── log.js
└── node_modules
1
2
3
4
5
6
7
// src/log.js
export const log = (name) => console.log(`Hello ${name}!`);

// src/index.js
import { log } from './log'

log('anran758');

src/log.js 导出了一个工具函数,它负责向控制台发送消息。src/index.js 是默认的入口文件,它引入 log 函数并调用了它。

上面的代码很简单,像这种模块化的代码按照传统 <script src> 引入的话,浏览器是不能正确执行的。可以在根目录上创建一个 index.html 引入 js 脚本来测试一下:

/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test</title>
</head>
<body>
<!-- 引入脚本 -->
<script src="./src/index.js"></script>
</body>
</html>

创建文件后,将上例代码复制到 index.html 中。保存并打开该文件,看看浏览器能否正确处理模块逻辑。不出意外的话,文件在浏览器打开后,浏览器开发者工具会抛出错误信息:

1
Uncaught SyntaxError: Cannot use import statement outside a module

言下之意就是说浏览器不能正确的解析 ES module 语句,此时 webpack 就可以派上用场啦~ 在 package.json 中的 scripts 字段中添加如下命令:

/package.json
1
2
3
4
  "scripts": {
+ "build": "webpack"
- "test": "echo \"Error: no test specified\" && exit 1"
},

在命令行输入 npm run build 调用 webpack 对当前项目进行编译,编译后的结果会输出到 dist/main.js 文件中(即便本地没有 dist 目录,它都会自动创建该目录)。输出文件后,修改 index.html 对 js 的引用:

/index.html
1
2
3
4
  <body>
+ <script src="./dist/main.js"></script>
- <script src="./src/index.js"></script>
</body>

重新刷新页面后就能看到 log 正确的输出了 Hello anran758!。点击 log 右侧的链接,可以跳转至 Source 面板,将代码格式化后可以清晰地看到编译后 js 的变化:

使用配置

当然,上例代码只不过是小试牛刀。对于正式的项目会有更复杂的需求,因此需要自定义配置。webpack 主要有两种方式接收配置:

第一种: 通过 Node.js API引入 webpack 包,在调用 webpack 函数时传入配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
const webpack = require("webpack");

const webpackConfig = {
// webpack 配置对象
}

webpack(webpackConfig, (err, stats) => {
if (err || stats.hasErrors()) {
// 在这里处理错误
}

// 处理完成
});

第二种: 通过 webpack-cli 在终端使使用 webpack 时指定配置。

1
webpack [--config webpack.config.js]

两种方法内配置都是相似的,只是调用的形式不同。本篇先使用 webpack-cli 来做示例。

webpack 接受一个特定的配置文件,配置文件要求导出一个对象、函数、Promise 或多个配置对象组成的数组。

现在将上一章的 Demo 复制一份出来,并重命名为 **getting-started-config**,在该目录下新建 webpack.config.js 文件,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const path = require('path');

module.exports = {
// 起点或是应用程序的起点入口
entry: "./src/index",
output: {
// 编译后的输出路径
// 注意此处必须是绝对路径,不然 webpack 将会抛错(使用 Node.js 的 path 模块)
path: path.resolve(__dirname, "dist"),

// 输出 bundle 的名称
filename: "bundle.js",
}
}

上面的配置主要是定义了程序入口、编译后的文件输出目录。然后在 src/index.js 中修改一些内容用来打包后测试文件是否被正确被编译:

src/index.js
1
2
3
4
  import { log } from './log'

+ log('本节在测试配置噢');
- log('anran758');

随后在终端输入 num run build 进行编译,可以看到 dist 目录下多了个 bundle.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ npm run build
> webpack --config ./webpack.config.js

Hash: 3cd5f3bbfaf23f01de37
Version: webpack 4.43.0
Time: 117ms
Built at: 05/06/2020 1:01:37 PM
Asset Size Chunks Chunk Names
bundle.js 1010 bytes 0 [emitted] main
Entrypoint main = bundle.js
[0] ./src/index.js + 1 modules 123 bytes {0} [built]
| ./src/index.js 62 bytes [built]
| ./src/log.js 61 bytes [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

由于我们输出的文件名被修改了,此时还得修改 html 的引入路径。但每改一次输出目录,HTML 中的引入路径也得跟着改,这样替换的话就比较容易出纰漏。那能不能让 webpack 自动帮我们插入资源呢?答案是可以的。

Plugin

webpack 提供**插件(plugin)**的功能,它可以用于各种方式自定义 webpack 构建过程。

html-webpack-plugin 可以在运行 webpack 时自动生成一个 HTML 文件,并将打包后的 js 代码自动插入到文档中。下面来安装它:

1
npm i --D html-webpack-plugin

安装后在 webpack.config.js 中使用该插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  const path = require('path');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
// 起点或是应用程序的起点入口
entry: "./src/index",

// 输出配置
output: {
// 编译后的输出路径
// 注意此处必须是绝对路径,不然 webpack 将会抛错(使用 Node.js 的 path 模块)
path: path.resolve(__dirname, "dist"),

// 输出 bundle 的名称
filename: "bundle.js",
},
+ plugins: [
+ new HtmlWebpackPlugin({
+ title: 'Test Configuration'
+ })
+ ],
}

重新编译后 HTML 也被输出到 dist 目录下。查看 dist/index.html 的源码可以发现:不仅源码被压缩了,同时 <script> 标签也正确的引入了 bundle.js

此时目录结构如下:

后续目录展示会将 node_modulespackage-lock.jsonyarn.lock 这种对项目架构讲解影响不大的目录省略掉..

1
2
3
4
5
6
7
8
9
10
11
.
├── dist
│ ├── bundle.js
│ ├── index.html
│ └── main.js
├── index.html
├── package.json
├── src
│ ├── index.js
│ └── log.js
└── webpack.config.js

处理完资源自动插入的问题后,还有一个问题需要我们处理:虽然 webpack 现在能自动生成 HTML 并插入脚本,但我们还得在 HTML 中写其他代码逻辑呀,总不能去改 /dist/index.html 文件吧?

这个问题也很好解决。html-webpack-plugin 在初始化实例时,传入的配置中可以加上 template 属性来指定模板。配置后直接在指定模板上进行编码就可以解决这个问题了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
// 起点或是应用程序的起点入口
entry: "./src/index",

// 输出配置
output: {
// 编译后的输出路径
// 注意此处必须是绝对路径,不然 webpack 将会抛错(使用 Node.js 的 path 模块)
path: path.resolve(__dirname, "dist"),

// 输出 bundle 的名称
filename: "bundle.js",
},
plugins: [
// html-webpack-plugin
// https://github.com/jantimon/html-webpack-plugin#configuration
new HtmlWebpackPlugin({
title: 'Test Configuration',
+ template: path.resolve(__dirname, "./index.html"),
})
],
}

使用模板后 html-webpack-plugin 也会自动将脚本插入到模板中。因此可以将模板中的 <script> 给去掉了。为了测试输出的文件是否使用了模板,在 <body> 内随便插入一句话,重新打包后预览输出的文件是否包含这句话:

/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Test Config</title>
- <title>Test</title>
</head>
<body>
+ <p>Test Config</p>
- <script src="./dist/main.js"></script>
</body>
</html>

修改文件后,重新打包就能看到模板也被压缩输出至 /dist/index.html 了,script 标签也正常的插入了。

清理目录

现在来看编译后的目录,我们发现 dist/mian.js 这文件是使用配置之前编译出来的文件,现在我们的项目已经不再需要它了。这种历史遗留的旧文件就应该在每次编译之前就被扔进垃圾桶,只输出最新的结果。

clean-webpack-pluginrimraf 可以完成清理功能。前者是比较流行的 webpack 清除插件,后者是通用的 unix 删除命令(安装该依赖包后 windows 平台也能用)。如果仅是清理 /dist 目录下文件的话,个人是比较倾向使用 rimraf的,因为它更小更灵活。而 clean-webpack-plugin 是针对 webpack 输出做的一系列操作。

在终端安装依赖:

1
npm i -D rimraf

rimraf 的命令行的语法是: rimraf <path> [<path> ...],我们在 package.jsonscirpts 中修改 build 的命令:

/package.json
1
2
3
4
"scripts": {
+ "build": "rimraf ./dist && webpack --config ./webpack.config.js"
- "build": "webpack --config ./webpack.config.js"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ npm run build

> rimraf ./dist && webpack --config ./webpack.config.js

Hash: 763fe4b004e1c33c6876
Version: webpack 4.43.0
Time: 342ms
Built at: 05/06/2020 2:35:49 PM
Asset Size Chunks Chunk Names
bundle.js 1010 bytes 0 [emitted] main
index.html 209 bytes [emitted]
Entrypoint main = bundle.js
[0] ./src/index.js + 1 modules 123 bytes {0} [built]
| ./src/index.js 62 bytes [built]
| ./src/log.js 61 bytes [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
Child HtmlWebpackCompiler:
1 asset
Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
1 module

这样 webpack 输出的 /dist 目录始终是最新的东西。

loader

在正常的页面中,引入 css 样式表会让页面变得更美观。引入图片可以让页面内容更丰富。

然而 webpack 本体只能处理原生的 JavaScript 模块,你让它处理 css 或图片资源,它是无法直接处理的。为了处理这种问题,webpack 提供了 loader 的机制,用于对模块外的源码进行转换。

loader 一般是单独的包,我们可以在社区找到对应 loader 来处理特定的资源。在使用前通过 npm 安装到项目的开发依赖中即可。loader 可以通过配置内联Cli 这三种方式来使用。下文主要以 配置 的方式来使用。

css

往常引入 css 样式表无非就是在 html 中通过 <link> 标签引入。现在想通过 webpack 来管理依赖得需要安装对应的 loader 来处理这些事。

css-loader 可以让 webpack 可以引入 css 资源。光有让 webpack 识别 css 的能还不够。为了能将 css 资源进行导出,还要安装 mini-css-extract-plugin 插件:

现在将上一节的 Demo 复制并重名为 getting-started-loader-css。进入新的项目目录后安装依赖:

1
npm install -D css-loader mini-css-extract-plugin

在更改配置之前,为了使项目结构更清晰,咱们按照文件类型重新调整源码目录结构。将 src 下的 js 文件都放进 js 文件夹中。同时创建 /src/css/style.css 样式表。调整后的目录结构如下:

1
2
3
4
5
6
7
8
9
10
.
├── package.json
├── src
│ ├── index.html
│ ├── css
│ │ └── style.css
│ └── js
│ ├── index.js
│ └── log.js
└── webpack.config.js

现在将 Flexbox 布局用例 中结尾的 Demo 迁移到项目中,测试一下效果:

HTML 源码
/src/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test</title>
</head>
<body>
<div class="panels">
<div class="panel panel1">
<p class="item name">Alice</p>
<p class="item index">I</p>
<p class="item desc">Pixiv Content ID: 65843704</p>
</div>
<div class="panel panel2">
<p class="item name">Birthday</p>
<p class="item index">II</p>
<p class="item desc">Pixiv Content ID: 70487844</p>
</div>
<div class="panel panel3">
<p class="item name">Dream</p>
<p class="item index">III</p>
<p class="item desc">Pixiv Content ID: 65040104</p>
</div>
<div class="panel panel4">
<p class="item name">Daliy</p>
<p class="item index">IV</p>
<p class="item desc">Pixiv Content ID: 64702860</p>
</div>
<div class="panel panel5">
<p class="item name">Schoolyard</p>
<p class="item index">V</p>
<p class="item desc">Pixiv Content ID: 67270728</p>
</div>
</div>
</body>
</html>
CSS 源码
/src/css/style.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
html {
font-family: 'helvetica neue';
font-size: 20px;
font-weight: 200;
background: #f7f7f7;
}

body,
p {
margin: 0;
}

.panels {
display: flex;
min-height: 100vh;
overflow: hidden;
}

.panel {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
color: white;
background: #ececec;
text-align: center;
box-shadow: inset 0 0 0 5px rgba(255, 255, 255, 0.1);
transition: font-size 0.7s cubic-bezier(0.61, -0.19, 0.7, -0.11),
flex 0.7s cubic-bezier(0.61, -0.19, 0.7, -0.11), background 0.2s;
font-size: 20px;
background-size: cover;
background-position: center;
cursor: pointer;
}

.panel1 {
background-color: #f4f8ea;
}

.panel2 {
background-color: #fffcdd;
}

.panel3 {
background-color: #beddcf;
}

.panel4 {
background-color: ​#c3cbd8;
}

.panel5 {
background-color: #dfe0e4;
}

.item {
flex: 1 0 auto;
display: flex;
justify-content: center;
align-items: center;
transition: transform 0.5s;
font-size: 1.6em;
font-family: 'Amatic SC', cursive;
text-shadow: 0 0 4px rgba(0, 0, 0, 0.72), 0 0 14px rgba(0, 0, 0, 0.45);
}

.name {
transform: translateY(-100%);
}

.panel .index {
font-size: 4em !important;
width: 100%;
}

.desc {
transform: translateY(100%);
}

.open-active .name,
.open-active .desc {
transform: translateY(0);
width: 100%;
}

.panel.open {
flex: 3;
font-size: 40px;
}
JavaScript 源码
/src/js/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { log } from './log'
import '../css/style.css';

function installEvent() {
const panels = document.querySelectorAll('.panel')

function toggleOpen() {
panels.forEach(item => {
if (item === this) return;
item.classList.remove('open')
});

this.classList.toggle('open');
}

function toggleActicon(e) {
if (e.propertyName.includes('flex-grow')) {
this.classList.toggle('open-active')
}
}

// 给每个元素注册事件
panels.forEach(panel => {
panel.addEventListener('click', toggleOpen)
panel.addEventListener('transitionend', toggleActicon)
})
}

installEvent();
log('本节在测试配置噢');

修改 webpack 配置,引入 css-loadermini-css-extract-plugin。既然已经对源码目录进行分类了,那顺便也给输出目录的文件也进行分类整理吧:

/webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
  const path = require('path');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
// 起点或是应用程序的起点入口
entry: "./src/js/index",

// 输出配置
output: {
// 编译后的输出路径
// 注意此处必须是绝对路径,不然 webpack 将会抛错(使用 Node.js 的 path 模块)
path: path.resolve(__dirname, "dist"),

// 输出 bundle 的名称
- filename: "bundle.js",
+ filename: "js/bundle.js",
+ },
+ module: {
+ rules: [
+ {
+ test: /\.css$/i,
+ use: [MiniCssExtractPlugin.loader, 'css-loader'],
+ },
+ ],
+ },
plugins: [
// html-webpack-plugin
// https://github.com/jantimon/html-webpack-plugin#configuration
new HtmlWebpackPlugin({
title: 'Test Configuration',
- template: path.resolve(__dirname, "./index.html"),
+ template: path.resolve(__dirname, "./src/index.html"),
+ }),
+
+ // 提取 css 到单独的文件
+ // https://github.com/webpack-contrib/mini-css-extract-plugin
+ new MiniCssExtractPlugin({
+ // 选项类似于 webpackOptions.output 中的相同选项,该选项是可选的
+ filename: 'css/index.css',
+ })
],
}

现在我们根据上面的配置来解读 loader 的使用:

在上面的配置中,**module** 规定了如何处理项目中的不同类型的模块。**rules** 是创建模块时,匹配请求的 rule (规则)数组。rule 是一个对象,其中最常见的属性就是 testuseloader

rule.test 是匹配条件,通常会给它提供一个正则表达式或是由正则表达式组成的数组。如果配置了 test 属性,那这个 rule 将匹配指定条件。比如匹配条件写为 test: /\.css$/i,这意味着给后缀为 .css 的文件使用 loader

rule.use 顾名思义就是使用,给符合匹配条件的文件使用 loader。它可以接收一个字符串,这个字符串会通过 webpack 的 resolveLoader 选项进行解析。该选项可以不配置,它内置有解析规则。比如下例中默认会从 node_modules 中查找依赖:

1
use: 'css-loader'

rule.use 还可以是应用于模块的 UseEntry 对象。UseEntry 对象内主要有 loaderoptions 两个属性:

1
2
3
4
5
6
7
8
9
// use 传入 UseEntry 类型的对象
use: {
// 必选项,要告诉 webpack 使用什么 loader
loader: 'css-loader',
// 可选项,传递给 loader 选项
options: {
modules: true
}
},

如果 UseEntry 对象内只设置 loader 属性,那它与单传的字符串的效果是一样的。而 options 是传递给 loader 的配置项,不同 loader 会提供有不同的 options。值得注意的是,如果 use 是以对象形式传入,**loader 属性是必填的,而 options 是可选的**。

rule.use 还可以是一个函数,函数形参是正在加载的模块对象参数,最终该函数要返回 UseEntry 对象或数组:

1
2
3
4
5
6
7
8
9
10
11
use: (info) => {
console.log(info);
return {
loader: 'svgo-loader',
options: {
plugins: [{
cleanupIDs: { prefix: basename(info.resource) }
}]
}
}
}

打印出函数的形参 info 可以看到该对象拥有如下属性:

  • compiler: 当前的 webpack 编译器(可以未定义)
  • issuer: 导入正在加载的模块的模块的路径
  • realResource: 始终是要加载的模块的路径
  • resource: 要加载的模块的路径,通常等于 realResource。除非在请求字符串中通过 !=! 覆盖资源名。

由此可见,使用函数方式可用于按模块更改 loader 选项。

rule.use 最常见的使用形式还是提供一个数组,数组中每项可以是字符串、UseEntry 对象、UseEntry 函数。这也是一个套娃的过程:

1
use: [{ loader: MiniCssExtractPlugin.loader }, 'css-loader'],

这里需要注意的是,**rule 中使用多个 loader 要注意其顺序。使用数组 loader 将会从右至左进行应用**。

比如上例中最先通过 css-loader 来处理 .css 文件的引入问题,再通过 MiniCssExtractPlugin.loader (Tips: 该值是 loader 的绝对路径)来提取出文件。如果反过来应用就会出问题了,webpack 都不知道如何引用 css 文件,自然提取不出东西啦。

rule.loaderrule.use 的缩写,等价于 rule.use: [{ loader }]。webpack 像这样简写的配置属性还有很多,这样做有利也有弊。对于熟手来说,提供这种简便选项可以减少配置的嵌套关系,但对新手来说,这配置有种错综复杂的感觉。

1
2
3
4
5
6
{
// 匹配文件规则
test: /\.css$/i,
// rule.use 简写形式
loader: 'css-loader'
}

接下来回归正题。重新编译 webpack,编译后的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
├── dist
│ ├── css
│ │ └── index.css
│ ├── index.html
│ └── js
│ └── bundle.js
├── package.json
├── src
│ ├── css
│ │ └── style.css
│ ├── index.html
│ └── js
│ ├── index.js
│ └── log.js
└── webpack.config.js

image

图片资源也是项目中的常见资源,引入图片资源同样需要安装 loader。处理图片资源的 loader 主要有两种,分别是 url-loaderfile-loader

file-loader

file-loader 是将 import/require() 引入的文件解析为 url,并把文件输出到输出目录中。

复制一份新 Demo 并重命名为 **getting-started-loader-images**。在安装 loader 之前先做一个小优化:

如果我们会频繁修改源码文件,修改完后又要重新编译,这个步骤实际是有点繁琐的。webpack 有个 watch 选项可以监听文件变化,若文件有修改 webpack 将自动编译(若修改的是配置文件的话,还是需要重新运行命令)。

package.jsonscript 中给 webpack 添加 -w 选项:

1
2
3
"scripts": {
"build:watch": "rimraf ./dist && webpack --config ./webpack.config.js -w"
},

接下来就可以安装依赖了:

1
npm i -D file-loader

新建一个 /src/images 文件夹,往里面添加一些图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  .
├── package.json
├── src
│ ├── css
│ │ └── style.css
+ │ ├── images
+ │ │ ├── 01.jpg
+ │ │ ├── 02.png
+ │ │ ├── 03.jpg
+ │ │ ├── 04.png
+ │ │ ├── 05.png
+ │ │ ├── 06.jpg
+ │ │ ├── webpack.jpg
+ │ │ └── webpack.svg
│ ├── index.html
│ └── js
│ ├── index.js
│ └── log.js
└── webpack.config.js

webpack.config.js 中配置 loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  rules: [
{
test: /\.html$/i,
loader: 'html-loader',
},
{
// 匹配文件规则
test: /\.css$/i,
// use 从右至左进行应用
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
+ {
+ test: /\.(png|jpe?g|gif|webp|svg)(\?.*)?$/,
+ use: {
+ loader: 'file-loader',
+ options: {
+ name: 'img/[name].[hash:8].[ext]'
+ },
+ },
+ },
],

默认情况下图片会被输出到 dist 目录中,文件名也会被更改为一长串的哈希值。为了保持目录整洁,将要被输出的图片资源都归类到 img 目录中。

可以通过设定 namepublicPath 来指定目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 直接设置 name
use: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]',
},
},

// 或者使用 publicPath,效果与上例等价
use: {
loader: 'file-loader',
options: {
publicPath: 'img',
name: '[name].[hash:8].[ext]',
},
},

name 属性的值可以用 / 分层。除去最末尾一层的是文件名,前面每层 / 分隔都是嵌套的文件夹。比如值为 static/img/[name].[hash:8].[ext] 最后输出的结果是:根目录创建一个 static 目录,static 内又会创建一个 img 目录,img 内输出被引用的图片资源。

由于匹配的图片资源有很多,咱们不能写死输出的文件名,不然会引发重名问题,操作系统不准这样干。这时 **占位符(placeholder)**就能排上用场了。name 中方括号包裹起来的是占位符,不同占位符会被替换成不同的信息。

比如上例中使用了三个占位符: name 是文件的名称、hash 是指定用于对文件内容进行 hash (哈希)处理的 hash 方法,后面冒号加数值代表截取 hash 的长度为 8、ext 是文件的扩展名。在文件名加入 hash 的用意是针对浏览器缓存而特意加入的。现在可以不用在意这种优化问题,未来会专门另起一篇文章讲优化的问题。

现在修改完 webapck 配置,接着再来完善上一节的 Demo。在 /src/css/styles.css 中使用 backgournd-image 引入图片:

css 引入图片资源
/src/css/style.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 省略其他代码... */
.panel1 {
background-color: #f4f8ea;
background-image: url('../images/01.jpg');
}

.panel2 {
background-color: #fffcdd;
background-image: url('../images/02.png');
}

.panel3 {
background-color: #beddcf;
background-image: url('../images/03.jpg');
}

.panel4 {
background-color: ​#c3cbd8;
background-image: url('../images/04.png');
}

.panel5 {
background-color: #dfe0e4;
background-image: url('../images/05.png');
}

重新编译后的结果如下:

编译结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
> rimraf ./dist && webpack --config ./webpack.config.js -w


webpack is watching the files…

Hash: 398663f1f4d417d17c94
Version: webpack 4.43.0
Time: 1086ms
Built at: 05/29/2020 2:19:03 PM
Asset Size Chunks Chunk Names
css/index.css 1.72 KiB 0 [emitted] main
img/01.a8e7ddb2.jpg 170 KiB [emitted]
img/02.46713ed3.png 744 KiB [emitted] [big]
img/03.70b4bb75.jpg 529 KiB [emitted] [big]
img/04.b7d3aa38.png 368 KiB [emitted] [big]
img/05.875a8bc2.png 499 KiB [emitted] [big]
index.html 990 bytes [emitted]
js/bundle.js 1.33 KiB 0 [emitted] main
Entrypoint main = css/index.css js/bundle.js
[0] ./src/css/style.css 39 bytes {0} [built]
[1] ./src/js/index.js + 1 modules 938 bytes {0} [built]
| ./src/js/index.js 873 bytes [built]
| ./src/js/log.js 60 bytes [built]
+ 1 hidden module

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
img/04.b7d3aa38.png (368 KiB)
img/05.875a8bc2.png (499 KiB)
img/02.46713ed3.png (744 KiB)
img/03.70b4bb75.jpg (529 KiB)

WARNING in webpack performance recommendations:
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit https://webpack.js.org/guides/code-splitting/
Child HtmlWebpackCompiler:
1 asset
Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
[0] ./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html 1.01 KiB {0} [built]
Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!src/css/style.css:
Entrypoint mini-css-extract-plugin = *
[0] ./node_modules/css-loader/dist/cjs.js!./src/css/style.css 3.09 KiB {0} [built]
[3] ./src/images/01.jpg 63 bytes {0} [built]
[4] ./src/images/02.png 63 bytes {0} [built]
[5] ./src/images/03.jpg 63 bytes {0} [built]
[6] ./src/images/04.png 63 bytes {0} [built]
[7] ./src/images/05.png 63 bytes {0} [built]
+ 2 hidden modules

当我们重新打开 /dist/index.html 时会发现图片并没有加载出来?查看 css 源码后发现原来是路径有问题,编译后的路径是 img/01.a8e7ddb2.jpg 这种相对路径。

由于 css 本身有一个文件夹,通过相对路径引入,那就会从 css 目录下进行查找。实际找到的是 dist/css/img/01.a8e7ddb2.jpg 这条路径。

遇到这种情况怎么办呢?我们可以给 MiniCssExtractPlugin.loader 添加 publicPath 选项用以修正路径,重新编译后就可以看到图片正确被加载了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
// 匹配文件规则
test: /\.css$/i,
// use 从右至左进行应用
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../',
}
},
'css-loader'
],
},

在 js 中也可以引用文件,打开 /src/js/index.js, 在原先的基础上添加如下代码:

1
2
3
4
5
6
7
8
9
10
import img1 from '../images/06.jpg';
import img2 from '../images/webpack.jpg';
import img3 from '../images/webpack.svg';

// 省略其他代码...

log('测试图片引入~');
console.log('img1 --> ', img1);
console.log('img2 --> ', img2);
console.log('img3 --> ', img3);

重新编译后可以在 Console 面板可以看到 js 输出了文件资源的路径:

url-loader

url-loader 功能也类似于 file-loader,不同的是当文件大小(单位 byte)小于 limit 时,可以返回一个 DataURL

为什么要用 DataURL 呢?我们知道页面上每加载一个图片资源,都会发起一个 HTTP 请求。而建立 HTTP 请求的过程是需要花时间的。因此可以将文件转为 DataURL 嵌入 html/css/js 文件中,这样可以有效减少 HTTP 建立连接时所带来额外的时间开销了。同时 html/css/js 文件也可以被浏览器缓存,DataURL 被引入后也能一同被缓存。

图片转 DataURL 也有缺点,那就是编码后文本储存所占的空间比图片会更大。这其实就是传输体积与 HTTP 连接数的权衡。所以最佳做法是将小图片转为 DataURL,转换后并不会有过多体积溢出,而大尺寸图片照常引入即可。

安装 url-loader:

1
npm install url-loader -D

修改 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
rules: [
{
// 匹配文件规则
test: /\.css$/i,
// use 从右至左进行应用
use: [
{
loader: MiniCssExtractPlugin.loader,
options: { publicPath: '../' }
},
'css-loader'
],
},
{
test: /\.(png|jpe?g|gif|webp)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
limit: 10000,
name: 'img/[name].[hash:8].[ext]'
},
},
},
{
test: /\.(svg)(\?.*)?$/,
use: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
},
},
},
],

在上例中将 pngjpgjpeggifwebp 文件交给 url-loader 处理,而 svg 仍由 file-loader 处理。这样做的理由是: DataURL 内联 svg 会破坏 sprite 系统 (将多个 svg 合为一张使用的技术) 中使用的Fragment Identifiers,因此不将 svg 转为 DataURL

url-loader 设定匹配规则后,配置 namelimit 选项。url-loadername 选项与 file-loadername 作用是相同的,就不再累述。

limit 是指定以字节(byte) 为单位的文件最大尺寸。当文件尺寸小于等于 limit 所设的值,那文件将会被转为 DataURL。相反,若文件尺寸大于 limit 时,则使用备用 loader。默认备用 loaderfile-loader。可以设定 fallback 选项来修改备用 loader

1
2
3
4
5
6
7
8
{
loader: 'url-loader',
options: {
limit: 10000,
name: 'img/[name].[hash:8].[ext]'
fallback: 'file-loader'
}
}

limit 的选值不易过大,可以设为 10240 (10KB)或 10000,也可以根据项目实际情况进行调整。

现在来测试 limit 的效果。unix 系统可以在终端使用 ls -l 命令来查看文件信息:

1
2
3
4
5
6
7
8
9
10
11
➜  getting-started-loader-images git:(master) ✗ cd ./src/images
➜ images git:(master) ✗ ls -l
total 6144
-rwxr-xr-x 1 anran staff 173596 May 28 17:41 01.jpg
-rwxr-xr-x 1 anran staff 761560 May 28 17:41 02.png
-rwxr-xr-x 1 anran staff 542065 May 28 17:41 03.jpg
-rwxr-xr-x 1 anran staff 376562 May 28 17:41 04.png
-rwxr-xr-x 1 anran staff 510812 May 28 17:41 05.png
-rw-r--r-- 1 anran staff 760117 May 28 17:41 06.jpg
-rw-r--r--@ 1 anran staff 6943 May 30 13:54 webpack.jpg
-rw------- 1 anran staff 647 May 28 21:33 webpack.svg

从输出的信息可以看到 webpack.svg (647B) 和 webpack.jpg (6943B) 的文件尺寸都低于设定的 limit: 10000。由于 svg 文件不通过 url-loader 处理,那按照预想它将会被输出到 /dist/img 中。webpack.jpg 可以被 url-loader,那编译后应该被嵌入到 js 代码中。

重新编译测试一下:

编译结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
➜  getting-started-loader-images git:(master) ✗ npm run build

> getting-started-loader@1.0.0 build /Users/anran/project_my/webpack-example/getting-started-loader-images
> rimraf ./dist && webpack --config ./webpack.config.js

Hash: 8d2e8c8220e86d46e388
Version: webpack 4.43.0
Time: 692ms
Built at: 05/30/2020 2:08:46 PM
Asset Size Chunks Chunk Names
css/index.css 1.63 KiB 0 [emitted] main
img/01.a8e7ddb2.jpg 170 KiB [emitted]
img/02.46713ed3.png 744 KiB [emitted] [big]
img/03.70b4bb75.jpg 529 KiB [emitted] [big]
img/04.b7d3aa38.png 368 KiB [emitted] [big]
img/05.875a8bc2.png 499 KiB [emitted] [big]
img/06.5b8e9d1e.jpg 742 KiB [emitted] [big]
img/webpack.258a5471.svg 647 bytes [emitted]
index.html 990 bytes [emitted]
js/bundle.js 10.5 KiB 0 [emitted] main
Entrypoint main = css/index.css js/bundle.js
[0] ./src/css/style.css 39 bytes {0} [built]
[1] ./src/js/index.js + 4 modules 10.1 KiB {0} [built]
| ./src/js/index.js 881 bytes [built]
| ./src/js/log.js 60 bytes [built]
| ./src/images/06.jpg 63 bytes [built]
| ./src/images/webpack.jpg 9.08 KiB [built]
| ./src/images/webpack.svg 68 bytes [built]
+ 1 hidden module

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
img/04.b7d3aa38.png (368 KiB)
img/03.70b4bb75.jpg (529 KiB)
img/05.875a8bc2.png (499 KiB)
img/02.46713ed3.png (744 KiB)
img/06.5b8e9d1e.jpg (742 KiB)

WARNING in webpack performance recommendations:
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit https://webpack.js.org/guides/code-splitting/
Child HtmlWebpackCompiler:
1 asset
Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
[0] ./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html 1.37 KiB {0} [built]
Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!src/css/style.css:
Entrypoint mini-css-extract-plugin = *
[0] ./node_modules/css-loader/dist/cjs.js!./src/css/style.css 2.98 KiB {0} [built]
[3] ./src/images/01.jpg 63 bytes {0} [built]
[4] ./src/images/02.png 63 bytes {0} [built]
[5] ./src/images/03.jpg 63 bytes {0} [built]
[6] ./src/images/04.png 63 bytes {0} [built]
[7] ./src/images/05.png 63 bytes {0} [built]
+ 2 hidden modules
编译后的目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
   .
├── dist
│ ├── css
│ │ └── index.css
│ ├── img
│ │ ├── 01.a8e7ddb2.jpg
│ │ ├── 02.46713ed3.png
│ │ ├── 03.70b4bb75.jpg
│ │ ├── 04.b7d3aa38.png
│ │ ├── 05.875a8bc2.png
│ │ ├── 06.5b8e9d1e.jpg
│ │ └── webpack.258a5471.svg
│ ├── index.html
│ └── js
│ └── bundle.js
├── package-lock.json
├── package.json
├── src
│ ├── css
│ │ └── style.css
│ ├── images
│ │ ├── 01.jpg
│ │ ├── 02.png
│ │ ├── 03.jpg
│ │ ├── 04.png
│ │ ├── 05.png
│ │ ├── 06.jpg
│ │ ├── webpack.jpg
│ │ └── webpack.svg
│ ├── index.html
│ └── js
│ ├── index.js
│ └── log.js
└── webpack.config.js

重新打开 /dist/index.html 后可以在浏览器控制台看到如下输出的信息:

HTML 资源引入

HTML 中有一种常见的情况是:在模板中通过相对路径引入图片、脚本等资源时,发现引入的资源都没有被打包进去。

为什么会发生这种情况呢?原来是 webpack 默认不会处理 html 中的资源引入。为了能使 HTML 能通过相对路径引入资源,主要有 3 种解决的方案:

lodash template

现在项目中 /src/index.html 是作为 html-webpack-plugin 的模板,在模板中可以使用 lodash template 语法(以下简称模板语法)来插入内容。语法格式为: <%= value %>

比如在 src/index.html 的模板中插入图片:

/src/index.html
1
2
3
4
5
6
<div class="panels">
<!-- 其他代码略... -->
<div class="panel panel6">
<img class="img" src="<%= require('./images/06.jpg').default %>" alt="">
</div>
</div>
/src/css/style.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* 为了使页面美观,再添加一些样式 */
.panel6 {
position: relative;
overflow: hidden;
background-color: #061927;
}

.panel6 .item {
position: relative;
}

.panel6 .img {
position: absolute;
height: 100%;
transform: scale(1);
transition: transform 0.4s 0.6s;
}

.panel6.open {
flex: 2;
}

.panel6.open .img {
transform: scale(1.2);
}

上例将通过 require() 函数引入图片。webpack 引入图片时默认是通过 ESModule 来引入的,因此解析的结果大致为 {default: module} 这种形式。因此后面还需要再加一个 default。这样就能正确的引入资源啦。

静态目录

第二种就是新增一个静态目录 static(或者叫 public)。

HTML 默认不是引用不了源码目录上的资源吗?那我就直接将资源输出到 dist 目录上。模板引用资源时直接引入输出后的文件不就行啦?

copy-webpack-plugin 可以完成这种迁移的功能。它将从 form 处复制文件/文件夹,复制到 to (默认是 webpack 的输出目录)中。现在来安装它:

1
npm i -D copy-webpack-plugin

新增 static 目录,并添加一些测试文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  .
├── package.json
├── src
│ ├── css
│ │ └── style.css
│ ├── images
│ │ ├── 01.jpg
│ │ ├── 02.png
│ │ ├── 03.jpg
│ │ ├── 04.png
│ │ ├── 05.png
│ │ ├── 06.jpg
│ │ ├── webpack.jpg
│ │ └── webpack.svg
│ ├── index.html
│ ├── js
│ │ ├── index.js
│ │ └── log.js
+ │ └── static
+ │ └── images
+ │ ├── 06.jpg
+ │ ├── webpack.jpg
+ │ └── webpack.svg
└── webpack.config.js

现在将 src/static/images 的所有文件(不管代码里有没有引入这些文件)都复制到 dist/img 中。

/webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
// webpack.config.js
{
plugins: [
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, 'src/static/images'),
to: path.resolve(__dirname, 'dist/img')
},
],
}),
],
}

如果你不仅想要复制图片还想要复制其他诸如 css 样式表、js 脚本甚至是 excel 文件到输出目录的话。那可以考虑将 static 目录与 dist 目录进行合并,将 staticdist 下的目录名保持一致。

比如将 static 的下 images 文件夹更名为图片输出目录 img,这样打包后会输出到同一个目录中:

/webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// webpack.config.js
{
plugins: [
new CopyPlugin({
patterns: [
// 如果只传 string 的话,那这个 string 相当于 from
// path.resolve(__dirname, 'src', 'static'),

// to 默认是 `compiler.options.output`, 也就是 dist 目录
// {
// from: path.resolve(__dirname, 'src/static'),
// to: ''
// },

// 当前配置中与上面两例等价
{
from: path.resolve(__dirname, 'src/static'),
to: path.resolve(__dirname, 'dist')
},
],
}),
],
}

若指定文件/文件夹不想复制到 dist 中,还可以使用 globOptions.ignore 来忽略:

/webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js
{
plugins: [
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, 'src/static'),
to: path.resolve(__dirname, 'dist')
globOptions: {
ignore: ['/**/webpack.jpg', '/**/img/webpack.svg'],
}
},
],
}),
],
}

重新修改模板中的图片的引入的路径,使其指向输出目录的 img:

1
2
3
4
<div class="panel panel6">
<img class="img" src="./img/06.jpg" alt="">
<p class="item index">VI</p>
</div>

编译后就能看到图片正确被引用了。

html-loader

最后一种是安装 html-loader,让 webapck 可以处理 html 资源的引入。

1
npm install -D html-loader
1
2
3
4
5
6
7
rules: [
{
test: /\.html$/i,
loader: 'html-loader',
},
// 省略其他 rule...
]

配置 html-loader 后,HTML 访问相对路径的资源就由 html-loader 来进行引入。将模板中的路径改为源码相对路径:

1
2
3
4
<div class="panel panel6">
<img class="img" src="./images/06.jpg" alt="">
<p class="item index">VI</p>
</div>

在实际编译时,<img class="img" src="./images/06.jpg" alt="">src 的值会被转为 require('./images/06.jpg'),通过 webpack 引入后再将编译后的结果传入图片的 src 属性中。

此时重新编译后就可以正确引入了。但配置 html-loader 的方法会与方法二访问静态目录资源有点冲突。配置 html-loader 后就不能通过 ./../ 这种相对路径来访问资输出目录的资源了。

如果我们配置了 html-loader 的同时又还想访问静态资源怎么办呢?这时可以通过根路径 / 逐层来访问,这样 html-loader 就不会处理这种路径:

1
2
3
4
 <div class="panel panel6">
<img class="img" src="/img/06.jpg" alt="">
<p class="item index">VI</p>
</div>

现在问题又来了,若我们通过根路径来访问资源的话,那就不能单纯地打开文件来在浏览器查看效果了。因为直接打开文件到浏览器上,是通过 file:// 协议打开的。浏览器实际上访问的路径是文件的绝对地址。

比如笔者打开文件后,浏览器地址栏展示的 url 是: file:///Users/anran/project_my/webpack-example/getting-started-static-assets/dist/index.html。现在通过根路径访问资源,需要浏览器补全为完整的 URL,经过浏览器补全后绝对路径是 file:///img/06.jpg。这样路径都是错误的自然就访问不到想要的资源啦。

如果有写过 SPA(单页面应用) 项目的朋友应该很熟悉。将 SPA 项目打包后直接访问 index.html 页面是空白的,这种情况多半就是从根路径引入资源失败而引起的。

这个问题解决的办法也很简单,就是将编译后的项目部署到服务器上,直接通过服务器进行访问,问题就迎刃而解了。为什么这样就可以解决了呢?

比如笔者的网站域名是 anran758.github.io,现在将页面部署到服务器后,直接在浏览器访问 https://anran758.github.io/,实际上访问的是 /dist/index.html 文件。html 通过相对路径访问/img/06.jpg,那补全后图片的路径就是 https://anran758.github.io/img/06.jpg。这样自然就能访问资源啦。

我们不妨通过 Node.js 起一个本地服务器测试一下。在 /dist 同级目录上新建一个 server.js 脚本,添加如下代码:

/server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const express = require('express');
const config = require('./webpack.config');

const app = express();
const PORT = 8001;

// 设置静态资源入口
app.use(express.static(config.output.path));

// 监听端口
app.listen(PORT, (err) => {
if (err) {
console.log(err);
return;
}

console.log('Listening at http://localhost:' + PORT + '\n');
})

上例脚本代码是通过 express 快速搭建一个本地服务器,将服务器静态资源入口设为 webpack.config.js 的输出目录(也就是 /dist),随后启动服务器。

express 是基于 Node.js 的 web 框架,要使用它之前需要安装依赖:

1
npm install -D express

package.json 中添加个快捷入口,并在终端运行该脚本:

1
2
3
4
5
6
{
"scripts": {
// 其他脚本..
"test:prod": "node server.js"
},
}
1
2
3
4
5
6
➜  getting-started-static-assets git:(master) ✗ npm run test:prod 

> getting-started-loader@1.0.0 test:prod /Users/anran/project_my/webpack-example/getting-started-static-assets
> node server.js

Server is running at http://localhost:8001 . Press Ctrl+C to stop.

打开 http://localhost:8001 后就能看到图片资源正确被引用了。

总结

好啦,现在 webpack 基础篇也到了尾声。我们对上述知识做一个简单的小结:

webpack 是一个静态模块打包工具,它本体虽然只支持处理 javascript 的模块,但可以通过 loader 让 webpack 完成原本它不能处理的功能。

webpack 的提供插件的功能,插件可以针对某种需求做特定处理,比如自动给 html 插入资源。

除了静态目录的文件外,我们发现 webpack 输出的文件都是有依赖关系的。为什么会这么说呢?仔细看看 webpack 处理的逻辑就能想清楚了:

webpack 从程序的入口 /src/js/index.js 开始处理,入口文件引入了 style.css,而 style.css 内又引用了图片资源。然后 HTML 再通过 webpack 插件引入模板,再将这些资源插入模板中。这就是文件的依赖关系,这些依赖关系最终会生成一个**依赖图(Dependency Graph)**。

想必看到这里的各位对 webpack 都有了个比较清晰的概念了吧?当然这只是一个开始,后面还有一些高级的概念在本文中由于篇幅的限制无法一并理清。若对笔者 webpack 的笔记感兴趣的话可以继续关注此系列的更新,下一篇将围绕开发环境进行梳理。

参考资料:

从搭建到部署,快速构建一个私人博客

作者 anran758
2019年8月19日 00:33

有时候我们希望有一个受控的博客,来记录或分享一些东西。这个博客的主题内容由你自己来决定,可以是技术分享(编程、汉化分享等),也可以是生活感想。

本文将介绍一个可以迅速搭建并部署的受控博客。阅读本文前,希望你对以下知识点有所了解:

  • git(版本控制) 的基础使用
  • markdown 的使用

为什么要搭建博客

在线类博客有很多选择,为什么我们需要从零搭建新的博客呢?自己搭建的博客有什么好处吗?

首先,前文所提的 “博客受控”,指的就是能够自己控制的博客的样式、内容等,自己想怎么改就怎么改。

内容受控是指我们知道在线类的博客是受平台限制的,这意味着你所发表的内容是需要受审才能发出的,一些敏感的技术词汇,该篇文章都可能会被和谐或被删除。但在自己搭建博客就没有这样的问题,最起码能保留源文件。

其二,博客的样式是受控的。像著名在线博客CSDN上一些博主的文章确实是有学习参考的价值,但问题的是该站广告是在是太多了,字体和排版的阅读体验并不太好。但如果是自己搭建的博客的话,就可以自己着手优化这些问题。

但博客的搭建还需要我们从各方面考虑利弊。平台类博客会有相应的推荐系统,会对同类型文章相互引流,在 SEO 方面会做得比我们好。

个人搭建的博客,刚起步时的浏览量并不高,但是可以通过SEO等方式来逐步增加自己网站的权重。或者提高博客的质量和干货,读者认为文章有价值,自然会收藏起来形成熟客。

那么博客能写什么东西呢?在日常生活中,有很多知识点是呈碎片状,写博客的本质上就是对自己知识的一种梳理,然后再将这些知识分享出来,可能会有对这方面知识有疑惑,或者想找到解决方案,自身分享出来的东西能给读者做一定的参考。同时这也会是一个良性循环,因为分享的同时,你可能也需要去查询一些资料,同时也可以找到别人遇到过并分享出来的解决方案,是一个相互收益的过程。

我们的基本需求是梳理与分享,那么更应该把注意力放在内容本身,网站布局的排版样式等则是增加读者阅读体验的问题。因此我们可以使用现成的博客框架快速完成这些事。

博客框架有很多种选择,笔者选择的是 Hexo,因为它足够便捷优雅。

start

Hexo 依赖 Node.jsNPM包管理,Node.js 安装后一般会自带NPM

我们打开终端(Windows PowerShell / cmd.exebashmacOS 里的终端),输入以下命令:

1
2
3
4
5
6
7
8
9
10
# 检查 npm 是否安装成功
npm -v

# 安装 hexo cli,
# 如果安装速度过慢的话,可以安装国内的淘宝镜像
# 在命令行输入 ` npm install -g cnpm --registry=https://registry.npm.taobao.org`
npm install -g hexo-cli

# 检查 hexo 是否安装成功,并查看版本
hexo -v

依赖安装成功后,我们可以在命令行输入 hexo help 查看使用方式(描述是英文,示例部分笔者将其转为中文):

Usage: hexo <command>

commanddescription
help获取命令的帮助
init创建一个新的 Hexo 文件夹
version显示版本信息

使用 hexo help [command] 可以查看更多的信息, 如:

1
2
3
4
5
6
7
8
9
10
11
12
hexo help init
# Usage: hexo init [destination]

# Description(描述):
# 在指定的路径或当前目录中创建一个新的Hexo文件夹.

# Arguments(参数):
# destination 文件夹路径。 如果未指定,则在当前文件夹中初始化

# Options(选项):
# --no-clone 复制文件而不是从GitHub克隆
# --no-install 跳过 npm 依赖安装(默认初始化会自动装依赖)

全局选项:

optionsdescription
--config指定配置文件而不是使用默认的 _config.yml
--cwd指定 CWD
--debug显示终端中的所有详细消息
--draft显示草稿帖子
--safe禁用所有插件和脚本
--silent在控制台上隐藏输出

在官网 commands 中可以找到全部完整的解释。

建站

在终端上,我们可以看到有一个 init 的命令,我们可以使用这个命令来初始化 hexo 项目,但再建站之前我们需要先决定在哪里存放博客源代码。

我推荐使用如微软的 OneDrive(win10 系统自带)之类的云文件夹。你可以白嫖它 5G 的云储存空间。当你在设备A下修改了文件,它会自动同步到云端上。切换回设备B并登录账号后,它又会自动从云端下载数据,是一个便捷的方式。

但值得注意的是 OneDrive 毕竟是国外服务,由于众所周知的原因可能需要科学上网才能使用。该方式只是数据备份与同步的问题,不使用它也不会影响下文的构建。

1
2
3
4
5
6
7
8
9
10
11
# 如果你是 unix 系统的话,可以使用该命令查看当前路径
pwd
# /Users/anran/OneDrive

# 初始化文件夹名为 blog
hexo init blog
# INFO Cloning hexo-starter https://github.com/hexojs/hexo-starter.git
# other install info ...

# 进入文件夹
cd blog

安装完成后目录如下:

1
2
3
4
5
6
7
8
9
.
├── _config.yml (网站的配置信息)
├── package.json (应用依赖信息)
├── node_modules (依赖包)
├── scaffolds (模板文件)
├── source (资源文件夹是存放用户资源的地方)
| ├── _drafts (草稿文件夹,刚初始化时可能不存在)
| └── _posts (文章/帖子源码列表)
└── themes (主题)

配置

建站完成后我们需要进行 配置hexo 中主要有两项配置。一项是站点配置文件,路径为 /_config.yml。另一项是主题配置文件,路径是/themes/(下载的主题)/_config.yml

我们可以先在站点配置文件修改以下基础选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Hexo Configuration

# 网站主标题,SEO元素之一
title: blog

#网站副标题,可选
subtitle:

# 网站描述, SEO元素之一,用于告诉搜索引擎关于这个站点的描述
description: 分享生活、分享技术

# 网站的关键词,如:
keywords: Front end

# 网站作者
author: anran758

# 网站使用的语言, 由于 Hexo 具备多语言配置,默认为英文,我们需要修改回中文语言
language: zh-CN

启动

初始化项目后默认会安装相关的依赖,接着在命令行输入如下命令来运行博客

1
2
3
4
5
6
7
8
# 启动服务,默认端口为 4000,启动服务后可以在浏览器输入 `http://localhost:4000` 查看效果
hexo server

# or 简写方式
hexo s

# 还可以使用 -p, 指定 9000 端口
hexo s -p 9000

写作步骤

我们一般通过命令行来操作博客:

比如创建文章的方式如下: hexo new [layout] <title>

layout是指定布局,Hexo默认有postpagedraft 三种布局,它们分别对应不同的路径。我们也可以自定义布局,但实际页面会和post相同,都将储存到source/_posts文件夹。

按照我个人的写作习惯,通常写作步骤是:

  1. 创建草稿(drafts)
  2. 在草稿上进行写作
  3. 整理细节并在本地服务器上查看效果(server)
  4. 发布至正式的帖子上
  5. 生成静态文件并部署(后续讲)

创建草稿(drafts)

1
2
$ hexo new draft "My first post"
# INFO Created: ~/blog/source/_drafts/My-first-post.md

在初次创建草稿会生成一个名为 _drafts 的草稿文件夹,接着该文件夹下有一个我们刚刚创建的草稿,名为 My-first-post.mdmarkdown 文件,文件内容如下:

1
2
3
4
---
title: My first post
tags:
---

在本地服务器查看草稿(drafts)

我们可以启动本地服务器一边写作一边预览,但默认情况下草稿是不会被展示出来的,如果你想查看草稿的话,可以输入以下命令:

1
2
3
4
5
$ hexo s -p 9000 --draft
# INFO Start processing
# INFO Hexo is running at http://localhost:9000 . Press Ctrl+C to stop.

# 如果需要退出服务器,按住 control + c

发布草稿(publish)

如果我们在本地服务器上校队完草稿细节后,可以将草稿发布为文章,否则在后续生成博客静态文件时不会被打包出来:

1
2
3
4
# hexo publish [layout] <filename>
# 将草稿发布为文章
$ hexo publish post My-first-post
# INFO Published: ~/blog/source/_posts/My-first-post.md

输入命令后你可以发现发布的文章被转移到了source/_posts/上,这样就完成了本地的文章发布。

生成静态文件(generate)

Hexo框架的一项工作就是将源文件 markdown 最后生成为 HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 生成文件
$ hexo generate
# INFO Start processing
# INFO Files loaded in 275 ms
# INFO Generated: 2019/08/11/My-first-post/index.html
# INFO 1 files generated in 152 ms

# 简写形式
$ hexo g

# 监控文件变化,并生成静态文件
$ hexo g --watch

# 生成文件并部署(部署后面单独章节来讲解)
$ hexo g -d

主题

我们熟悉完博客系统的操作后,接下来就是美化博客。Hexo 支持主题,我们可以根据官网的创建主题教程自己来设计,也可以直接在主题商城 中找现成的主题。这里以笔者推荐的主题 Next 为例:

theme next

笔者一开始使用 next 主题时,版本才 5.x,当时仍有很多博客所需的东西没有集成。如今回头一看,发现 next 升级了好几个大版本。github 主题仓库也迁移至了 https://github.com/theme-next 里,乃至文档都有两个不同的版本。

新文档是采用它自身主题的一个scheme来建成,是全英文文档,可以保证信息资料是最新的。旧文档布局便于阅读,同时是中文文档,大多参数也能在该文档找到,但毕竟没有再过多的维护,建议还是以最新文档为参考。

安装主题可以通过git clone克隆至blog/theme/下:

1
2
3
4
5
6
7
8
$ pwd
# /Users/anran/OneDrive/Blog

# 启动主题前需要清除缓存与已部署的文件
$ hexo clean

# clone 主题
$ git clone https://github.com/iissnan/hexo-theme-next themes/next

接着在 站点配置文件(/_config.yml) 中启动 theme。再打开主题配置文件(/themes/next/_config.yml)选择 Scheme:

1
2
3
4
5
6
7
8
9
# _config.yml
- theme: landscape
+ theme: next

# /themes/next/_config.yml
# 提供三种模式
#scheme: Muse
#scheme: Mist
scheme: Pisces

评论、订阅、数据统计、SEO 等部分功能配置已经集成至 next 主题配置中,但大多还需要额外添加依赖还需要根据文档来配置。next 在主题配置中集成了由于配置自定义项过多,读者可以根据自己所需添加相应的统计、SEO 相关的 app key 等就不进一步展开讲。

部署

我们使用git进行部署,可以将网站部署至私人服务器、也可以部署到免费的github pages上。本文将介绍部署至github的方法,如果你还没有github账号的话,那你需要先注册一个账号

步骤如下:

  1. 访问github.com,点击sign up注册账号。

  2. 进入注册页,输入账号密码和邮箱,输入验证码!

  3. 选择免费用户

  4. 接着是关于github推荐服务的调查,当然你也可以跳过它.

  5. 验证完毕后,它会提示你创建一个仓库,这里我们先创建一个blog

  6. 复制仓库链接,copy 至 站点配置文件(/_config.yml)里。同时安装hexo-deployer-git的依赖:

    1
    npm install hexo-deployer-git --save
    1
    2
    3
    4
    5
    6
    7
    url: https://yourname.github.io/blog   # 修改为 github io 的地址
    root: /blog/ # 要将资源映射到仓库名

    deploy:
    type: git
    repo: https://github.com/yourname/blog.git # blog 的 git 地址
    branch: gh-pages # 发布至 gp-pages 分支,如果该分支不存在,就会自动创建它
  7. 接着开始部署。如果你还没配置git账号的话,它会提示你输入账号密码,输入正确的账号密码后就部署成功了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    # 或者使用`hexo d -g`, 两者是等价的效果
    hexo g -d

    # *** Please tell me who you are.

    # Run
    # git config --global user.email "you@example.com"
    # git config --global user.name "Your Name"

    # to set your account's default identity.
    # Omit --global to set the identity only in this repository.

    # fatal: unable to auto-detect email address (got '29625@DESKTOP-0R7P8H4.(none)')
    # Logon failed, use ctrl+c to cancel basic credential prompt.
    # Username for 'https://github.com': anran758
    INFO Start processing
    INFO Files loaded in 621 ms
    INFO 0 files generated in 424 ms
    INFO Deploying: git
    INFO Clearing .deploy_git folder...
    INFO Copying files from public folder...
    INFO Copying files from extend dirs...

    INFO Congratulations! Your are using the latest version of theme NexT.
    Enumerating objects: 131, done.
    Counting objects: 100% (131/131), done.
    Delta compression using up to 8 threads
    Compressing objects: 100% (91/91), done.
    Writing objects: 100% (131/131), 257.72 KiB | 2.48 MiB/s, done.
    Total 131 (delta 43), reused 0 (delta 0)
    remote: Resolving deltas: 100% (43/43), done.
    To https://github.com/yourname/blog.git
    * [new branch] HEAD -> gh-pages
    Branch 'master' set up to track remote branch 'gh-pages' from 'https://github.com/yourname/blog.git'.

    # 如果没有配置全局 git 账号的话可以先配置,不然下次部署还是会提示你输入账号密码
    git config --global user.email "you@example.com"
    git config --global user.name "Your Name"
  8. 接着在我们创建的blog下进入settings项,设置 github pagesgh-pages 也就是之前在配置里设置的分支即可。这样就可以在线上查看我们部署的状况啦~

优化与扩展

下面介绍一下文档中没有提到的相关问题与扩展。

本地搜索

next 有内置本地搜索的配置项,但文档上说明需要额外安装 hexo-generator-searchdb 这个依赖。但该项目现在已经被归档了,它还存在一些问题没有修复。你可以使用 hexo-generator-search 来代替它。接者在站点配置文件添加如下配置:

1
2
3
4
5
6
7
8
# Expansion: hexo-generator-search
# 站内搜索
# https://github.com/wzpan/hexo-generator-search
search:
path: search.xml
field: post
format: html
limit: 10000

在使用本地搜索功能时,你可能会遇到以下错误:

1
2
3
4
This page contains the following errors:
error on line 86 at column 35: Input is not proper UTF-8, indicate encoding !
Bytes: 0x08 0xE8 0xB7 0x9F
Below is a rendering of the page up to the first error.

出现这种错误原因大多是因为搜狗输入法带来的特殊字符串,我们在源码中替换它即可。打开编辑器(比如vscode),在全局搜索错误信息Bytes 第一个字节 /x08 替换为空。

github emoji

如果你希望在博客中支持 emoji 的话,你可以安装 hexo-filter-github-emojis

1
2
3
4
5
6
7
8
# Use Github Emojis
# Docs: https://github.com/crimx/hexo-filter-github-emojis
githubEmojis:
enable: true
className: github-emoji
unicode: false
styles:
localEmojis:

sitemap

为了让搜索引擎能找到我们的网站,我还需要给搜索引擎的网络蜘蛛提供站点地图文件

1
2
# hexo sitemap 生成器以及百度的 sitemap 生成器
npm install hexo-generator-sitemap hexo-generator-baidu-sitemap --save

依赖安装完后在站点配置文件中添加如下配置:

1
2
3
4
5
6
7
8
9
10
11
# Expansion: hexo-generator-sitemap
# generate sitemap.
# https://github.com/hexojs/hexo-generator-sitemap
sitemap:
path: sitemap.xml

# Expansion: hexo-generator-baidu-sitemap
# 针对百度进行优化的 sitemap,作者还是建议手动提交至百度会比较好
# https://github.com/coneycode/hexo-generator-baidu-sitemap
baidusitemap:
path: baidusitemap.xml

设置无分页的归档

如果你期望将归档目录在一页中全部加载出来,那么你可以添加如下配置:

1
2
3
4
5
# hexo-generator-archive
# 该插件默认内置于 hexo 中,只需参考文档添加配置即可
# https://github.com/hexojs/hexo-generator-archive
archive_generator:
per_page: 0

图片的引入

hexo 中引用图片主要有两种方式:

  • 在本地通过资源文件夹引入
  • 使用图床

在本地资源的引入,需要修改 _config.yml 的配置:

1
post_asset_folder: true

设置完选项后,以后每次使用 hexo new [layout] <title> 后就会生成一个同名的文件夹。然后可以使用 `

` 来引入图片资源:
1
2
3
4
<!-- 例如插入一个 banner 图,hexo 会自动寻找同名文件夹下的文件 -->
{% asset_img banner.png banner %}

这里是一段示例内容。

该方法的缺点是需要占用本地资源,如果你是使用 git 进行部署,因为使 .git 文件变大(即便删除了该文件,它还会存在 git 的 commit 信息中)。

第二种方式可以使用图床,免费图床有个问题就是服务可能会不稳定,风险不由自己掌控,相对没那么保险。但是它能节省空间,甚至在网络传输上下载速度更快。如果使用图床的话,可以尝试新浪微博图床,将插件下载至 chrome,登录后即可上传得到相应的 url.

README

默认情况下,将源码生成部署至服务器会将上一次生成的数据覆盖掉。如果你期望在 github上保留一个 README.md 给读者看说明的话,可以通过 _config.yml 来设置它:

1
skip_render: ['images/loading.gif', 'README.md']

外链音乐播放器插件

网易云音乐提供了一个外链音乐播放器,可以插入博客中,样式以及播放的歌单都是通过url控制的。我们稍微封装一下,添加如下代码:

/next/_config.yml
1
2
3
4
5
6
7
8
9
10
11
12
# 网易云音乐插件
# 控制台: https://music.163.com/#/outchain/0/{% music.id %}/m/use/html
# 网易云音乐插件默认提供三种模式,不同模式设有不同宽高,样式也可能会略有不同。
# 如果你想调整默认的宽高的话,可以设置 width | height 覆盖原先 modal 的宽度
# modal: 1(310 X 430) | 2(310 X 90) | 3(278 | 32)
music:
enable: true
id: 102842761 # 网易云分享的ID
autoplay: false # 是否开启自动播放
modal: 2 # 模式, 默认为 modal 2
# width: 310 # 宽度, 默认为 modal 2
# height: 90 # 高度, 默认为 modal 2

添加 netease-cloud-music.swig 模板:

/next/layout/_partials/sidebar/netease-cloud-music.swig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{##
# extend layout: 网易云音乐外链播放器
# @author anran758
#}
{%- if theme.music and theme.music.enable %}
{# default size #}
{%- set music_model = theme.music.modal or 2 %}
{%- set music_width = 310 %}
{%- set music_height = 90 %}
{%- set music_padding = 20 %}

{# default config #}
{%- set music_id = theme.music.id or 102842761 %}
{%- set music_auto = 0 %}
{%- if theme.music.autoplay %}
{%- set music_auto = 1 %}
{%- endif %}

{%- if theme.music.width %}
{%- set music_width = theme.music.width %}
{%- elif music_model === 1 or music_model === 2 %}
{%- set music_width = 310 %}
{%- elif music_model === 3 %}
{%- set music_width = 278 %}
{%- endif %}

{%- if theme.music.height %}
{%- set music_height = theme.music.height %}
{%- elif music_model === 1 %}
{%- set music_height = 430 %}
{%- elif music_model === 2 %}
{%- set music_height = 90 %}
{%- elif music_model === 3 %}
{%- set music_height = 32 %}
{%- endif %}

<iframe
frameborder="no"
border="0"
marginwidth="0"
marginheight="0"
width="{{ music_width + music_padding}}"
height="{{ music_height + music_padding }}"
src="//music.163.com/outchain/player?type=0&id={{ music_id }}&auto={{ music_auto }}&height={{ music_height }}"
style="margin: 10px 0 50px;"
></iframe>
{%- endif %}

layout/_macro/sidebar.swig 中插入模板:

layout/_macro/sidebar.swig
1
2
3
4
5
6
7
8
<div class="site-overview-wrap sidebar-panel">
{{ partial('_partials/sidebar/site-overview.swig', {}, {cache: theme.cache.enable}) }}

<!-- 插入下列代码 -->
{{ partial('_partials/sidebar/netease-cloud-music.swig', {}, {cache: theme.cache.enable}) }}

{{- next_inject('sidebar') }}
</div>

不过值得注意的是,虽然插件能用,但由于博客渲染的是多页面,如果是跳到了别的页面,原先播放状态就会被破坏,从用户体验来看,这个功能并不太实用。


Hexo 的介绍就说到这里了,虽然还有一些技巧相关的内容,如果有读者感兴趣的话到时候再深入讲吧~

❌
❌