多页面WebApp项目脚手架方案

传统的 web 网站,是多页面的,不借助新出的框架,比如 react 或者 vue 等。

这里给出一个脚手架项目,可以 clone 后,具备:

  • 使用 webpack/babel 将 es 语法编译为兼容低版本浏览器语法
  • 开发模式下,web 页面随着程序改动自动更新
  • 生产模式下
    • 压缩 JavaScript 和 CSS 文件,自动引用到 html 页面
    • 编译输出可发布的所有静态文件
    • html 文件配置强制不缓存
    • js/css 文件名带 hash 值,每次发布的文件名称不同,方便做 cdn
    • 多个页面需要的公共库,比如 jQuery,单独编译,在多页面共享

本文参考了 Static website, multiple html pages, using Webpack + example on Github


创建空项目#

创建名为multiple-page-webapp-scaffold的目录,在该目录下执行命令,初始化 npm 项目:

1
npm init

创建目录#

创建基本目录和文件:

1
mkdir -p src/{css,scripts,partials,page-index}

目录的作用:

  • css,全局的 css 文件,多页面复用
  • scripts,全局的 javascript 文件,多页面复用
  • partials,html 页面片段,页眉、页脚和导航条等
  • page-index,index.html 和它的 css 和 javascript

基本文件#

在这些目录下创建空文件:

1
touch src/{css/main.css,scripts/utils.js,partials/footer.html,page-index/{tmpl.html,main.js,main.css}}

还需要一些控制文件:

1
touch ./{.editorconfig,.eslintrc.js,.nvmrc,.gitignore,README.md,webpack.dev.js,webpack.prod.js}

控制文件#

控制文件的作用:

  • .editorconfig, http://editorconfig.org
  • .eslintrc.js, eslint 配置
  • .nvmrc, 项目使用 node 的版本号
  • .gitignore, git 忽略文件
  • webpack
    • webpack.dev.js 开发环境的配置文件
    • webpack.prod.js 生产环境的配置文件

完整目录文件树#

目录和文件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
multiple-page-webapp-scaffold/
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── .nvmrc
├── README.md
├── package.json
├── src
│   ├── css
│   │   └── main.css
│   ├── page-index
│   │   ├── main.css
│   │   ├── main.js
│   │   └── tmpl.html
│   ├── partials
│   │   └── footer.html
│   └── scripts
│   └── utils.js
├── webpack.dev.js
└── webpack.prod.js

Git 和版本控制#

初始化 git 项目:

1
git init

gitignore.io 的文件内容填充.gitignore文件。


编写和运行首页#

最简单的首页#

编写一个简单的首页,src/page-index/tmpl.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<title>首页</title>
<meta charset="UTF-8" />
<meta http-equiv="cache-control" content="no-cache" />
<meta http-equiv="pragma" content="no-cache" />
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta
name="viewport"
content="width=device-width,user-scalable=0,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0"
/>
<link rel="shortcut icon" href="#" />
</head>
<body>
<main>
<h1>
首页
</h1>
</main>
</body>
</html>

编写 page-index/main.js#

page-index/main.js中填写:

1
2
3
4
5
6
7
class Foo {
constructor() {
alert("create new foo");
}
}

new Foo();

如果main.js文件被加载,会触发警告窗口。

运行最基本的 webpack 的开发环境#

为了让 webpack 运行起来,需要以下操作:

  • 安装 webpack 所需库
  • 编写webpack.dev.js
  • 在 package.json 加入 webpack 运行命令
  • 运行 npm 命令,浏览器访问首页

安装 webpack 相关库#

执行 npm 命令:

1
npm install -D webpack webpack-cli webpack-dev-server html-webpack-plugin

编写 webpack.dev.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
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpack = require("webpack");

module.exports = {
devtool: "eval-cheap-module-source-map",

entry: {
index: "./src/page-index/main.js"
},

devServer: {
port: 8080
},

module: {
rules: []
},

plugins: [
new HtmlWebpackPlugin({
template: "./src/page-index/tmpl.html",
inject: true,
chunks: ["index"],
filename: "index.html"
})
]
};

在 package.json 加入 webpack 运行命令#

package.json文件中加入:

1
2
3
4
5
6
7
8
9
{
"name": "multiple-page-webapp-scaffold",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server --config webpack.dev.js --mode development" //加入这句
},
...

运行 npm 命令,浏览器访问首页#

执行命令:

1
npm start

运行成功后,在浏览器打开:http://localhost:8080/

这时候改动下首页(src/page-index/tmpl.html)的内容,浏览器页面应该立即改动生效。

可以看到page-index/main.js代码执行了。

webpack 自动在生成的 html 文件中注入了这个 js 文件:

1
2
3
4
5
...
</main>
<script type="text/javascript" src="index.js"></script>
</body>
...

在 javascript 入口文件中引用 js 文件#

目前的配置,不但能将作为入口的 javascript(本示例中是page-index/main.js)注入到 html 页面中,也支持加载入口 js 文件引用的其他 javascript 库。

比如假定有page-index/hello.js,可以在page-index/main.js中引用:

1
import hello from "./hello";

加载 page-index/main.css#

目前的环境,只能加载页面目录下的 javascript 文件,还不能加载 css 文件。

下面将创建一个最简单的 css 文件并配置 webpack 加载它。

编写最简版本的 page-index/main.css#

page-index/main.css:

1
2
3
main {
background-color: bisque;
}

在 javascript 入口文件中引入 main.css#

page-index/main.js:

1
2
import "./main.css";
...

编辑 webpack.dev.js 支持 css 加载#

需要安装 webpack 插件:

1
npm i -D css-loader style-loader

webpack.dev.js 加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
...
module: {
rules: [
{
test: /\.css$/,
use: [
"style-loader",
"css-loader"`
]
}
]
},
...

首页中引入 partials/footer.html#

footer.html 应该是个单独的页面,被其他页面引用。

编写最简 footer.html#

partials/footer.html:

1
<footer>marshal@ohtly</footer>

在 page-index/tmpl.html 中引入 footer.html#

在需要放置 footer.html 内容的位置加入;

1
<%= require('html-loader!../partials/footer.html') %>

安装所需插件 html-loader#

需要安装所需插件:

1
npm i -D html-loader

无需配置文件,就可以使功能生效。

html-loader 使用的模版引擎是:blueimp,语法很像 jsp。

到此为止,能做什么?不能做什么?#

能做什么:

  • import 其他 css 文件,也可以 import 其他 css 库,比如normalize.css(通过 npm install 安装)
  • import 其他 js 文件,也可以 import 其他 js 库,这些库通过 npm install 安装
  • 定义页眉页脚,以及导航条等页面,然后在网页中将它们引入进来复用

总之,和开发相关的基本功能都满足了。

不能做什么?

  • 和生产环境相关的功能,比如
    • 编译为可部署的网页静态文件
    • 针对 js/css 对低版本浏览器和不同引擎浏览器的编译支持
    • 文件的合并和压缩,因为减少了 http 请求数,减小了文件大小,加载会更快
    • 图片的压缩处理,比如对很小的图片文件,通过 base64 编码内联到 html 页面里去
    • js 和 css 文件,针对 cdn 的配置,文件名要保持唯一性
    • 有些通用的 js 库文件和 css,不应该编译到每个页面对应的 js/css 文件里去,应单独编译,发挥 cdn 和浏览器缓存作用

生产环境下其实需要配置的还很多,这里列出了基本的部分,也是下文要说明的内容。


生产构建的配置#

最简生产构建的配置#

要求能将项目编译出可用的静态文件即可。

安装所需库和编写 webpack.prod.js 文件#

安装 webpack 构建所需的库:

1
npm i -D html-webpack-plugin mini-css-extract-plugin optimize-css-assets-webpack-plugin clean-webpack-plugin

最小化的 webpack.prod.js#

编写 webpack.prod.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
const path = require("path");

const CleanWebpackPlugin = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin");

const buildPath = path.resolve(__dirname, "dist");

module.exports = {
entry: {
index: "./src/page-index/main.js"
},

output: {
filename: "[name].js",
path: buildPath
},

module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
}
]
},

plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: "./src/page-index/tmpl.html",
inject: "body",
chunks: ["index"],
filename: "index.html"
}),
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
})
],

optimization: {
minimizer: [new OptimizeCssAssetsPlugin({})]
}
};

webpack.prod.js 做的事情#

配置文件主要做的事情:

  • 编译构建的文件,放置在dist目录下
  • 每次运行构建,都会清空dist目录,重新创建编译后的文件
  • 生成 css 文件(index.css),合并所有当前页面的 css 到一个文件中,并且进行了压缩
  • 生成 js 文件(index.js),合并所有当前页面的 js 到一个文件中
  • 生成 html 页面(首页,index.html),并将 css 和 js 文件引入到 html 文件中

package.json 的配置以及运行构建命令#

package.json中加入命令,调用 webpack 执行,使用webpack.prod.js的配置:

1
2
3
4
5
...
"scripts": {
"start": "webpack-dev-server --config webpack.dev.js --mode development",
"build": "webpack --config webpack.prod.js --mode production" // 增加的内容
...

构建生成文件的检查#

生成的文件:

1
2
3
4
dist
├── index.css
├── index.html
└── index.js

可以分别查看下文件内容,对照检查是否实现上面提到的功能。

压缩 js 文件#

需要安装:

1
npm i -D uglifyjs-webpack-plugin babel-loader @babel/core @babel/preset-env

然后配置webpack.prod.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
// 需要require相关库,这里忽略了
..
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
},
{ // 增加这个规则配置,babel将es6语法做处理,否则最小化的时候会不识别而报错
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
}
]
..
optimization: {
minimizer: [
new UglifyJsPlugin({ //优化部分加上这个插件,是做最小化js文件的
cache: true,
parallel: true,
sourceMap: true
}),
new OptimizeCssAssetsPlugin({})

执行命令npm run build,然后检查 js 文件是否做了压缩。

生成 map source#

一般在 js 文件做了压缩以后,要生成 map source 文件,目的是方便出现问题后确定报错在源代码的位置。

配置很简单,webpack.prod.js

1
2
3
4
..
module.exports = {
devtool: "source-map", // 增加这行
..

这样在构建后,会增加index.js.map文件。

有了它,打开 chrome dev tools 查看报错时,就会定位到还原后的 js 源文件的位置了。

需要注意的几点:

  • source map 文件可能会很大,如果 js 文件很多的话,但是没关系,除非使用比如 chrome dev tools 这样的开发工具,是不会加载这个文件的
  • 为了安全或者版权,是否需要取消这个操作
    • 即使有人得到源代码,反向工程的难度也大于正向工程,意义不大
    • 生成 source map,我认为是必选项,不然运行中的 web 服务,排错会很困难
    • source map 就是为了正式运行服务出现错误而设计的,本地开发调试不会用到它
    • 如果实在无法拒绝客户的这个无理要求,可以设置 map source 只能在 vpn 情况下访问,开发可以通过 vpn 访问该网页

source map 是必选项,这是为什么我要单独用一个小节说它。

编译兼容浏览器版本的 js#

有时候需要对版本较低的浏览器兼容,需要 babel 提前对 js 文件做相应的处理。

这需要进一步设置@babel/preset-env

比如,希望兼容市占率(当然是国外的情况) 1%以上的浏览器,编辑.babelrc

1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": ["> 1%"]
}
}
]
]
}

如果指定具体浏览器的最低版本,比如 ie9:

1
2
3
4
5
6
7
8
9
10
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"ie": "9"
}
}
]

css 自动增加前缀的处理#

有一些标准 css 规则关键字在浏览器中带厂商前缀,autoprefixer#webpack提供了配置方式。

因为 css 在不同开发场景下差异较大,这里就不做处理了。

多页面复用相同 js 和 css#

比如,jQuery(js 库)和 Bootstrap(js 和 css 库)。

如果不能多个页面复用同一个文件,会造成不必要的带宽/流量的浪费。

针对 jQuery 的处理#

在当前配置下增加使用 jQuery 的代码#

安装 jQuery 库

1
npm i jquery

在 js 文件中引用 jQuery,page-index/main.js:

1
2
3
4
5
6
7
8
..
import $ from "jquery";

class Foo {
constructor() {
alert("create new foo");
$("#desc").html("使用jquery加入的文字");
..

然后执行构建命令,会在dist/index.js中找到 jQuery 的代码部分。

jQuery 生成单独的文件#

配置webpack.prod.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
...
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: "./src/page-index/tmpl.html",
inject: "body",
chunks: ["vendor", "index"], // 这行增加vendor
filename: "index.html"
}),
...
optimization: {
minimizer: [
..
],
splitChunks: {
cacheGroups: {
vendor: {
name: "vendor",
test: /[\\/]node_modules[\\/]/,
chunks: "initial",
priority: 1
}
}
}
...

执行构建命令,会看到生成了新的文件:

1
2
3
4
5
6
7
dist
├── index.css
├── index.html
├── index.js
├── index.js.map
├── vendor.js // 新增加的
└── vendor.js.map // 新增加的

针对 Bootstrap 的处理#

bootstrap 库的安装(bootstrap 依赖 jquery,之前已经安装):

1
npm i popper.js bootstrap

引用方法见: bootstrap/webpack

比如类似这样:

1
2
import "bootstrap/js/dist/util"; // js
import "bootstrap/dist/css/bootstrap.min.css"; // css

可以在 html 中增加一个元素,看 bootstrap 的 css 是否生效:

1
2
3
<div class="alert alert-primary" role="alert">
A simple primary alert—check it out!
</div>

针对 cdn 的配置#

cdn 的作用#

在提供大规模广域网服务的情况下,使用 cdn 可以

  • 提高用户界面加载速度,提升用户体验效果
  • 显著减少主站的流量消耗,节约流量付费成本

js 和 css 每次构建使用唯一性的文件名#

使用 cdn 的情况下,css 和 js 文件,一般都需要设置为长期缓存的。

如果更新这些文件,因为 cdn 的存在,会造成用户加载不同步的问题。

解决办法就是:

  • 每次构建,js 和 css 使用唯一命名的文件名
  • 同时,加载 js 和 css 的 html 文件,一定不要允许 http 缓存

编辑webpack.prod.js:

1
2
3
4
output: {
filename: "[name].[contenthash].js", // 增加[contenthash].
path: buildPath
},

在配置文件中相关 js 和 css 文件名配置增加[contenthash].

执行构建后,将看到类似这样的文件名:

1
2
3
4
5
6
7
8
dist/
├── 1.86980fa9bc6155305ae7.css
├── index.277d61082906d19d34ed.js
├── index.277d61082906d19d34ed.js.map
├── index.920ef0a39b56a4292fc3.css
├── index.html
├── vendor.bcb48b8c7b1167e522e6.js
└── vendor.bcb48b8c7b1167e522e6.js.map

contenthash,根据文件内容取 hash,因此如果文件没有改动,则重新构建的文件名不变。

对图片文件加载的优化#

需要安装:

1
npm i -D url-loader

在 html 引用图片链接:

1
2
3
4
<div><img src="<%=require('./cat.jpg')%>" /></div>
//当前tmpl.html路径下
<div><img src="<%=require('../assets/castle.jpg')%>" /></div>
//其他目录下

webpack.dev.js中这样配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
..
module: {
rules: [
{
test: /\.css$/,
use: [
"style-loader",
"css-loader"
]
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: "url-loader",
options: {
name: "[path][name].[ext]?hash=[hash:20]",
limit: 8192
}
}
..

webpack.prod.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
..
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
},
{
test: /\.(png|jpg|gif|svg)$/,
use: [
{
loader: "url-loader",
options: {
name: "[name].[contenthash].[ext]",
limit: 8192
}
}
..

需要完善的事情#

时间有限,还有一些需要完善的事情,列在下面,随时补充完善:

  • 源代码提交到Github上去
  • html和资源文件(图片等)的目录层级关系
  • 增加preview,加载dist的页面