前端工程化


title: 前端工程化

date: 2019-09-01 14:26:50

tags: javascript

categories:  # 这里写的分类会自动汇集到 categories 页面上,分类可以多级

  • JS # 一级分类
  • 前端工程化 # 二级分类

模块化初探

模块化系统的风格

模块化的价值

最主要的目的:

  • 解决命名冲突
  • 依赖管理

其他价值:

  • 提高代码可读性
  • 代码解耦,提高复用性

<script>标签风格

1
2
3
<script src="module1.js"></script>
<script src="module2.js"></script>
<script src="module3.js"></script>

各个模块把接口暴露给全局对象,比如 window.各个模块通过全局对象进行相互访问

出现的问题:

  1. 全局对象的冲突
  2. 加载的顺序很重要
  3. 开发者需要解决模块的依赖问题
  4. 在大项目中模块非常多难以维护

CommonJs: 同步的 require

CommonJs 是 Node 独有的规范,浏览器中使用就需要用到 Browserify 解析了。

使用同步的require方法来加载依赖和返回暴露的接口.一个模块可以通过exports对象添加属性,或者设置module.exports的值来描述暴露对象.

1
2
3
4
require("moudle");
require("../file.js");
exports.doStuff = function () {};
moudle.exports = someValue;

优点:

  1. 服务端代码可以被复用.
  2. npm 有大量的代码
  3. 使用方便

缺点:

  1. 阻塞调用无法在网络环境应用,网络请求是异步的
  2. 不能并行require多个模块

CommonJS 规范

  1. 在一个模块中,存在一个自由的变量”require”,它是一个函数。

这个”require”函数接收一个模块标识符。

“require”返回外部模块所输出的 API。 2. 在一个模块中,会存在一个名为”exports”的自由变量,它是一个对象,模块可以在执行的时候把自身的 API 加入到其中。 3. 模块必须使用”exports”对象来做为输出的唯一表示。

AMD 规范: 异步的 require

AMD 主要解决两个问题:

  1. 多个 js 文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器
  2. js 加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长

requireJS 语法

RequireJS 遵循 AMD 规范,用于解决命名冲突和文件依赖的问题.

requireJS 定义了一个函数 define,它是全局变量,用来定义模块

define(id?, dependencies?, factory);

  1. id:可选参数,用来定义模块的标识,如果没有提供该参数,脚本文件名(去掉拓展名)
  2. dependencies:是一个当前模块依赖的模块名称数组
  3. factory:工厂方法,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值

在页面上使用 require 函数加载模块

require([dependencies], function(){});

require()函数接受两个参数:

  1. 第一个参数是一个数组,表示所依赖的模块
  2. 第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块.

require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

1
2
3
4
5
6
require(["moudle", "../file"], function (moudle, file) {
/* ... */
});
define("moudle", ["dep1", "dep2"], function (d1, d2) {
return someExportedValue;
});

importexport的区别

以前:

CommonJS(服务器) 和 AMD(浏览器)规范各自实现了运行时加载模块的方法(没办法在编译时做“静态优化”)。

1
2
3
4
5
6
7
// CommonJS模块
let { stat, exists, readFile } = require("fs");
// 等同于
let _fs = require("fs");
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

现在:

ES6:一个模块就是一个文件,export/import命令可以出现在模块的任何位置,只要处于模块顶层就可以。

通过importexport实现静态加载(编译时加载),服务端和浏览器通用。

1
2
// ES6模块
import { stat, exists, readFile } from "fs";

export:如果你希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量

1
2
3
4
5
// profile.js
var firstName = "Michael";
var lastName = "Jackson";
var year = 1958;
export { firstName, lastName, year };

还可以输出函数和 class 类:

1
2
3
export function multiply(x, y) {
return x * y;
}

还可以改个名字输出:

1
2
3
4
5
6
7
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};

需要特别注意的是,export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

export 语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

CommonJS 模块输出的是值的缓存,不存在动态更新。

import:加载模块

1
2
3
4
5
// main.js
import { firstName, lastName, year } from "./profile";
function setName(element) {
element.textContent = firstName + " " + lastName;
}

可以改名字:

1
import { lastName as surname } from "./profile";

注意,import 命令具有提升效果,会提升到整个模块的头部,首先执行。import 命令是编译阶段执行的,在代码运行之前。

由于 import 是静态执行,所以不能使用表达式和变量。

会执行加载的模块:

1
import "lodash"; //仅仅执行lodash模块,但是不输入任何值。多次引入同一个模块仅执行一次。

整体加载所有的模块:

1
import * as circle from "./circle";

export default:为模块指定默认输出,引用的时候不用知道输出的到底是什么,可以指定任意名字。

需要注意的是,这时 import 命令后面,不使用大括号。一个模块只能有一个默认输出。

1
2
3
4
5
6
7
// export-default.js
export default function () {
console.log("foo");
}
// import-default.js
import customName from "./export-default";
customName(); // 'foo'

同时引用默认模块和其他模块:

1
2
3
4
5
6
7
8
9
10
//输出
export default function (obj) {
// ···
}
export function each(obj, iterator, context) {
// ···
}
export { each as forEach };
//对应的加载
import _, { each, each as forEach } from "lodash";

ES6 模块加载 CommonJS 模块:

1
2
3
4
5
6
7
8
9
10
11
//commonjs模块:
// a.js
module.exports = {
foo: "hello",
bar: "world",
};
// 等同于
export default {
foo: "hello",
bar: "world",
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//es6加载上面的模块:
// 写法一
import baz from "./a";
// baz = {foo: 'hello', bar: 'world'};
// 写法二
import { default as baz } from "./a";
// baz = {foo: 'hello', bar: 'world'};
// 写法三
import * as baz from "./a";
// baz = {
// get default() {return module.exports;},
// get foo() {return this.default.foo}.bind(baz),
// get bar() {return this.default.bar}.bind(baz)
// }

CommonJS 加载 ES6 模块:通过import()函数

1
2
3
4
// es6.mjs
let foo = { bar: "my-default" };
export default foo;
foo = null;
1
2
3
4
5
6
7
8
9
// commonjs.js
const es_namespace = await import("./es");
// es_namespace = {
// get default() {
// ...
// }
// }
console.log(es_namespace.default);
// { bar:'my-default' }

import 和 require 的区别

#module.exports 和 export default 的区别

webpack

webpack 是收把项目当作一个整体,通过一个给定的的主文件,webpack 将从这个文件开始找到你的项目的所有依赖文件,使用 loaders 处理它们,最后打包成一个或多个浏览器可识别的 js 文件.

初始化

  1. 新建一个文件夹,cd 到它的目录下.执行npm init -y命令
  2. 执行npm install --save--dev webpack命令安装(也可以全局安装)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/配置
touch webpack.config.js
vi webpack.config.js
//在里面写以下内容
/*
const path = require('path');
module.exports = {
entry: './src/index.js', //入口
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};*/

//创建文件
touch src/index.js

//运行webpack
npx webpack //这时会多出dist目录,里面有bundle.js文件

2.使用

1.在 index.js 里写

1
2
3
4
5
6
console.log(1)

//再运行webpack:
npx webpack

//再看bundle.js,这时会多出来一行console.log(1)

2.安装 babel-loader 自动转换 es6

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
//安装v6,命令行
npm install babel-loader babel-core babel-preset-env webpack

//将这个复制到webpack的配置文件webpack.config.js里,加在output的下面
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['env']
}
}
}
]
}

//加在output的下面,复制完后成这样
const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['env']
}
}
}
]
}
};

运行 npx webpack

若出现 can’t find ‘…’或 can’t resolve ‘…’的报错,则安装省略号里的东西 npm i ‘省略号’

注意:若是 Couldn’t find preset “env”,不要安装 env,而是 npm i babel-preset-env

3.使用 babel

1
2
3
//当你在写index.js里写
let a = 1;
//它就会帮你自动转换成es5了

NPM 脚本(NPM Scripts)

在 package.json 文件中写入 npm 脚本,就可以使用npm run build替代npx命令.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  {
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
+ "build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^4.0.1",
"webpack-cli": "^2.0.9",
"lodash": "^4.17.5"
}
}

管理资源(css,图片之类)

加载 CSS

  1. 需要先安装 style-laoder 和 css-loader

npm install --save-dev style-loader css-loader

  1. 然后在webpack.config.js文件里添加规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: [
+ 'style-loader',
+ 'css-loader'
+ ]
+ }
+ ]
+ }
};

webpack 根据正则表达式,来确定应该查找哪些文件,并将其提供给指定的 loader。在这种情况下,以 .css 结尾的全部文件,都将被提供给 style-loader 和 css-loader。

这使你可以在依赖于此样式的文件中import './style.css'。现在,当该模块运行时,含有 CSS 字符串的<style>标签,将被插入到 html 文件的 <head>中。

加载图片

  1. 安装npm install --save-dev file-loader插件
  2. 配置webpack.config.js文件

现在,当你 import MyImage from './my-image.png',该图像将被处理并添加到 output 目录,

并且 MyImage 变量将包含该图像在处理后的最终 url。当使用 css-loader 时,如上所示,

你的 CSS 中的url('./my-image.png') 会使用类似的过程去处理。loader 会识别这是一个本地文件,

并将 './my-image.png'路径,替换为输出目录中图像的最终路径。

html-loader 以相同的方式处理 <img src="./my-image.png" />

  1. 在 src 文件夹下创建 icon.png.

  2. 修改src/index.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  import _ from 'lodash';
import './style.css';
+ import Icon from './icon.png';

function component() {
var element = document.createElement('div');

// Lodash,现在由此脚本导入
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');

+ // 将图像添加到我们现有的 div。
+ var myIcon = new Image();
+ myIcon.src = Icon;
+
+ element.appendChild(myIcon);

return element;
}

document.body.appendChild(component());
  1. src/style.css中引入图片
1
2
3
4
  .hello {
color: red;
+ background: url('./icon.png');
}
  1. 重新构建npm run build

Glup

简介

它是一款 nodejs 应用。

它是打造前端工作流的利器,打包、压缩、合并、git、远程操作…,

简单易用

无快不破

高质量的插件

安装

安装 gulp

npm install -g gulp

如果报Error: EACCES, open '/Users/xxx/xxx.lock错误。

先执行:sudo chown -R $(whoami) $HOME/.npm

如果使用 npm 安装插件太慢(被墙),

可执行 npm install -g cnpm --registry=https://registry.npm.taobao.org

先安装 cnpm, 之后再安装插件时用 cnpm 安装cnpm install gulp

  1. 安装各种插件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
npm install --save gulp            //本地使用gulp
npm install --save gulp-imagemin //压缩图片
npm install --save gulp-minify-css //压缩css
npm install --save gulp-ruby-sass //sass
npm install --save gulp-jshint //js代码检测
npm install --save gulp-uglify //js压缩
npm install --save gulp-concat //文件合并
npm install --save gulp-rename //文件重命名
npm install --save png-sprite //png合并
npm install --save gulp-htmlmin //压缩html
npm install --save gulp-clean //清空文件夹
npm install --save browser-sync //文件修改浏览器自动刷新
npm install --save gulp-shell //执行shell命令
npm install --save gulp-ssh //操作远程机器
npm install --save run-sequence //task顺序执行

或者根据 package.json 自动安装。把 package.json 拷贝到自己的工程目录下,进入目录,执行:npm install

语法 gulp API

gulp.src(globs[,options])

1
2
3
4
5
gulp
.src("client/templates/*.jade")
.pipe(jade())
.pipe(minify()) //压缩文件
.pipe(gulp.dest("build/minified_templates"));

作用: 输出符合所提供的匹配模式或数组的文件.

globs 类型: string 或 Array

所要读取的 glob 或者包含 globs 的数组.可以是地址

options 类型:object

额外的选项参数

gulp.dest(path[,options])

1
2
3
4
5
6
gulp
.src("./client/templates/*.jade")
.pipe(jade())
.pipe(gulp.dest("./build/templates"))
.pipe(minify())
.pipe(gulp.dest("./build/minified_templates"));

文件被 pipe 进来,dest 生成或者写入一个文件.

path

文件将被写入的路径(输出目录)。

gulp.task(name[,deps],fn)

定义一个实现任务

1
2
3
gulp.task('somename', function(){
//任务
})

name

任务名字

deps 类型: array

一个包含任务列表的数组,这些任务会在你当前运行任务之前完成.

1
2
3
gulp.task('mytask', ['array', 'of', 'task', 'names'], function(){
//任务
})

fn

该函数定义任务所要执行的一些操作。通常来说,它会是这种形式:gulp.src().pipe(someplugin())。

gulp.watch(glob[, opts], tasks)

监视文件,并且可以在文件发生改动时候做一些事情。它总会返回一个 EventEmitter 来发射(emit) change 事件。

1
2
3
4
5
6
var watcher = gulp.watch("js/**/*.js", ["uglify", "reload"]);
watcher.on("change", function (event) {
console.log(
"File " + event.path + " was " + event.type + ", running tasks..."
);
});

gulp 使用实例

范例 1. 压缩合并

demo1 目录结构如下。把 demo1 中的 index.html 压缩,把 src 里面的 less 编译、合并、压缩、重命名、存储到 dist。src 里面的图片压缩、合并存储到 dist。src 里面的 js 做代码检查,压缩,合并,存储到 dist。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ demo1
+ dist
+ css
- merge.min.css
+ js
- merge.min.js
+ imgs
- 1.png
- 2.png
- index.html
+ src
+ css
- a.css
- b.css
+ js
- a.js
- b.js
+ imgs
- 1.png
- 2.png
- index.html

创建 gulpfile.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
var gulp = require("gulp");

// 引入组件
var minifycss = require("gulp-minify-css"), // CSS压缩
uglify = require("gulp-uglify"), // js压缩
concat = require("gulp-concat"), // 合并文件
rename = require("gulp-rename"), // 重命名
clean = require("gulp-clean"), //清空文件夹
minhtml = require("gulp-htmlmin"), //html压缩
jshint = require("gulp-jshint"), //js代码规范性检查
imagemin = require("gulp-imagemin"); //图片压缩

gulp.task("html", function () {
return gulp
.src("src/*.html")
.pipe(minhtml({ collapseWhitespace: true })) //html压缩
.pipe(gulp.dest("dist"));
});

gulp.task("css", function (argument) {
gulp
.src("src/css/*.css")
.pipe(concat("merge.css")) // CSS压缩
.pipe(
rename({
suffix: ".min", // 重命名
})
)
.pipe(minifycss()) // CSS压缩
.pipe(gulp.dest("dist/css/"));
});
gulp.task("js", function (argument) {
gulp
.src("src/js/*.js")
.pipe(jshint()) //js代码规范性检查
.pipe(jshint.reporter("default"))
.pipe(concat("merge.js")) // 合并文件
.pipe(
rename({
suffix: ".min", // 重命名
})
)
.pipe(uglify()) // js压缩
.pipe(gulp.dest("dist/js/"));
});

gulp.task("img", function (argument) {
gulp
.src("src/imgs/*")
.pipe(imagemin()) //图片压缩
.pipe(gulp.dest("dist/imgs"));
});

gulp.task("clear", function () {
gulp.src("dist/*", { read: false }).pipe(clean()); //清空文件夹
});

gulp.task("build", ["html", "css", "js", "img"]);

最后命令行gulp build;

可实现 src 目录下的 html 压缩,css、js 合并压缩,图片压缩,最后放入 dist 目录下

范例 2. 监控变动自动同步

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
var gulp = require("gulp");

// 引入组件
var browserSync = require("browser-sync").create(); //用于浏览器自动刷新
var scp = require("gulp-scp2"); //用于scp到远程机器
var fs = require("fs");

gulp.task("reload", function () {
browserSync.reload();
});

gulp.task("server", function () {
browserSync.init({
server: {
baseDir: "./src",
},
});

gulp.watch(["**/*.css", "**/*.js", "**/*.html"], ["reload", "scp"]);
});

gulp.task("scp", function () {
return gulp
.src("src/**/*")
.pipe(
scp({
host: "121.40.201.213",
username: "root",
privateKey: fs.readFileSync("/Users/wingo/.ssh/id_rsa"),
dest: "/var/www/fe.jirengu.com",
watch: function (client) {
client.on("write", function (o) {
console.log("write %s", o.destination);
});
},
})
)
.on("error", function (err) {
console.log(err);
});
});

命令行执行:

1
2
gulp scp;  // 可把本地开发环境代码拷贝到服务器
gulp server; //可在本地创建服务器,本地开发浏览器立刻刷新

范例 3.   监控项目文件变动,自动压缩、合并、打包、添加版本号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
html

</html>
<head>

<!-- build:css css/merge.css -->
<link href="css/a.css" rel="stylesheet">
<link href="css/b.css" rel="stylesheet">
<!-- endbuild -->

</head>
<body>

<p>demo1-工程化手动版</p>

<!-- build:js js/merge.js -->
<script type="text/javascript" src="js/a.js"></script>
<script type="text/javascript" src="js/b.js"></script>
<!-- endbuild -->

</body>
</html>

设置 gulpfile.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
var gulp = require("gulp");
var rev = require("gulp-rev"); //添加版本号
var revReplace = require("gulp-rev-replace"); //版本号替换
var useref = require("gulp-useref"); //解析html资源定位
var filter = require("gulp-filter"); //过滤数据
var uglify = require("gulp-uglify");
var csso = require("gulp-csso"); //css优化压缩
var clean = require("gulp-clean");

gulp.task("index", ["clear"], function () {
var jsFilter = filter("**/*.js", { restore: true });
var cssFilter = filter("**/*.css", { restore: true });

var userefAssets = useref.assets();

return gulp
.src("src/index.html")
.pipe(userefAssets) // Concatenate with gulp-useref
.pipe(jsFilter)
.pipe(uglify()) // Minify any javascript sources
.pipe(jsFilter.restore)
.pipe(cssFilter)
.pipe(csso()) // Minify any CSS sources
.pipe(cssFilter.restore)
.pipe(rev()) // Rename the concatenated files
.pipe(userefAssets.restore())
.pipe(useref())
.pipe(revReplace()) // Substitute in new filenames
.pipe(gulp.dest("dist"));
});

gulp.task("clear", function () {
gulp.src("dist/*", { read: false }).pipe(clean());
});

范例 4. 本地 shell 命令, 远程 shell, 任务顺序执行…

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
var gulp = require("gulp");

var shell = require("gulp-shell");
var runSequence = require("run-sequence");
var fs = require("fs");
var GulpSSH = require("gulp-ssh");

//shell操作,
gulp.task(
"git",
shell.task(["git add .", 'git commit -am "dd"', "git push -u origin dev"])
);
gulp.task(
"clear",
shell.task(['find . -name ".DS_Store" -depth -exec rm {} \\;'])
);

//操作远程主机
var gulpSSH = new GulpSSH({
ignoreErrors: false,
sshConfig: {
host: "121.40.201.213",
port: 22,
username: "root",
privateKey: fs.readFileSync("/Users/wingo/.ssh/id_rsa"),
},
});

gulp.task("remote", function () {
return gulpSSH.shell([
"cd /var/www/fe.jirengu.com",
"git pull origin dev",
"rm -rf _runtime",
]);
});

gulp.task("build", function (callback) {
runSequence("git", "clear", "remote", callback);
});

gulp.task("watch", function () {
gulp.watch(["**/*.css", "**/*.js", "**/*.html", "**/*.php"], ["build"]);
});

webpack 相关问题

  1. webpack 与 grunt、gulp 的不同

三者都是前端构建工具,grunt 和 gulp 在早期比较流行,现在 webpack 相对来说比较主流,不过一些轻量化的任务还是会用 gulp 来处理,比如单独打包 CSS 文件等。

grunt 和 gulp 是基于任务和流(Task、Stream)的。类似 jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个 web 的构建流程。

webpack 是基于入口的。webpack 会自动地递归解析入口所需要加载的所有资源文件,然后用不同的 Loader 来处理不同的文件,用 Plugin 来扩展 webpack 功能。

所以总结一下:

从构建思路来说

gulp 和 grunt 需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系

webpack 需要开发者找到入口,并需要清楚对于不同的资源应该使用什么 Loader 做何种解析和加工

对于知识背景来说

gulp 更像后端开发者的思路,需要对于整个流程了如指掌 webpack 更倾向于前端开发者的思路

  1. 与 webpack 类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用 webpack?

同样是基于入口的打包工具还有以下几个主流的:

webpack

rollup

parcel

从应用场景上来看:

webpack 适用于大型复杂的前端站点构建

rollup 适用于基础库的打包,如 vue、react

parcel 适用于简单的实验性项目,他可以满足低门槛的快速看到效果

由于 parcel 在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用 parcel

  1. 有哪些常见的 Loader?他们是解决什么问题的?

file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件

url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去

source-map-loader:加载额外的 Source Map 文件,以方便断点调试

image-loader:加载并且压缩图片文件

babel-loader:把 ES6 转换成 ES5

css-loader:加载 CSS,支持模块化、压缩、文件导入等特性

style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。

eslint-loader:通过 ESLint 检查 JavaScript 代码

  1. 有哪些常见的 Plugin?他们是解决什么问题的?

define-plugin:定义环境变量

commons-chunk-plugin:提取公共代码

uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码

  1. Loader 和 Plugin 的不同?

不同的作用

Loader 直译为”加载器”。Webpack 将一切文件视为模块,但是 webpack 原生是只能解析 js 文件,如果想将其他文件也打包的话,就会用到 loader。 所以 Loader 的作用是让 webpack 拥有了加载和解析非 JavaScript 文件的能力

Plugin 直译为”插件”。Plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

不同的用法

Loader 在 module.rules 中配置,也就是它作为模块的解析规则而存在。 类型为数组,每一项都是一个 Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)

Plugin 在 plugins 中单独配置。 类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入。

  1. webpack 的构建流程是什么?从读取配置到输出文件这个过程尽量说全

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;

开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;

确定入口:根据配置中的 entry 找出所有的入口文件;

编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;

完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;

输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;

输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

  1. 是否写过 Loader 和 Plugin?描述一下编写 loader 或 plugin 的思路?

Loader 像一个”翻译官”把读到的源文件内容转义成新的文件内容,并且每个 Loader 通过链式操作,将源文件一步步翻译成想要的样子。

编写 Loader 时要遵循单一原则,每个 Loader 只做一种”转义”工作。 每个 Loader 的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用 this.callback()方法,将内容返回给 webpack。 还可以通过 this.async()生成一个 callback 函数,再用这个 callback 将处理后的内容输出出去。 此外 webpack 还为开发者准备了开发 loader 的工具函数集——loader-utils。

相对于 Loader 而言,Plugin 的编写就灵活了许多。 webpack 在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

  1. webpack 的热更新是如何做到的?说明其原理?

webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

原理:

首先要知道 server 端和 client 端都做了处理工作

第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。

第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API 对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。

第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了 devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。

第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。

webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。

HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。

而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。

最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

  1. 如何利用 webpack 来优化前端性能?(提高性能和体验)

用 webpack 优化前端性能是指优化 webpack 的输出结果,让打包的最终结果在浏览器运行快速高效。

压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用 webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS 文件, 利用 cssnano(css-loader?minimize)来压缩 css

利用 CDN 加速。在构建过程中,将引用的静态资源路径修改为 CDN 上对应的路径。可以利用 webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径

删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动 webpack 时追加参数–optimize-minimize 来实现

提取公共代码。

  1. 如何提高 webpack 的构建速度?

多入口情况下,使用 CommonsChunkPlugin 来提取公共代码

通过 externals 配置来提取常用库

利用 DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引用但是绝对不会修改的 npm 包来进行预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。

使用 Happypack 实现多线程加速编译

使用 webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采用了多核并行压缩来提升压缩速度

使用 Tree-shaking 和 Scope Hoisting 来剔除多余代码

  1. 怎么配置单页应用?怎么配置多页应用?

单页应用可以理解为 webpack 的标准模式,直接在 entry 中指定单页应用的入口即可,这里不再赘述

多页应用的话,可以使用 webpack 的 AutoWebPlugin 来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范。 多页应用中要注意的是:

每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。比如,每个页面都引用了同一套 css 样式表

随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置

  1. npm 打包时需要注意哪些?如何利用 webpack 来更好的构建?

Npm 是目前最大的 JavaScript 模块仓库,里面有来自全世界开发者上传的可复用模块。你可能只是 JS 模块的使用者,但是有些情况你也会去选择上传自己开发的模块。 关于 NPM 模块上传的方法可以去官网上进行学习,这里只讲解如何利用 webpack 来构建。

NPM 模块需要注意以下问题:

要支持 CommonJS 模块化规范,所以要求打包后的最后结果也遵守该规则。

Npm 模块使用者的环境是不确定的,很有可能并不支持 ES6,所以打包的最后结果应该是采用 ES5 编写的。并且如果 ES5 是经过转换的,请最好连同 SourceMap 一同上传。

Npm 包大小应该是尽量小(有些仓库会限制包大小)

发布的模块不能将依赖的模块也一同打包,应该让用户选择性的去自行安装。这样可以避免模块应用者再次打包时出现底层模块被重复打包的情况。

UI 组件类的模块应该将依赖的其它资源文件,例如.css 文件也需要包含在发布的模块里。

基于以上需要注意的问题,我们可以对于 webpack 配置做以下扩展和优化:

CommonJS 模块化规范的解决方案:

设置output.libraryTarget='commonjs2'使输出的代码符合 CommonJS2 模块化规范,以供给其它模块导入使用

输出 ES5 代码的解决方案:

使用 babel-loader 把 ES6 代码转换成 ES5 的代码。再通过开启devtool: 'source-map'输出 SourceMap 以发布调试。

Npm 包大小尽量小的解决方案:

Babel 在把 ES6 代码转换成 ES5 代码时会注入一些辅助函数,最终导致每个输出的文件中都包含这段辅助函数的代码,造成了代码的冗余。解决方法是修改.babelrc 文件,为其加入 transform-runtime 插件

不能将依赖模块打包到 NPM 模块中的解决方案:

使用 externals 配置项来告诉 webpack 哪些模块不需要打包。

对于依赖的资源文件打包的解决方案:通过 css-loader 和 extract-text-webpack-plugin 来实现,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
module: {
rules: [
{
//增加对CSS文件的支持
test: /\.css/,
//提取出 Chunk 中的CSS代码到单独的文件中
use: ExtractTextPlugin.extract({
use: ["css-loader"],
}),
},
],
},
plugins: [
new ExtractTextPlugin({
//输出的CSS文件名称
filename: "index.css",
}),
],
};
  1. 如何在 vue 项目中实现按需加载?

Vue UI 组件库的按需加载:

为了快速开发前端项目,经常会引入现成的 UI 组件库如 ElementUI、iView 等,但是他们的体积和他们所提供的功能一样,是很庞大的。 而通常情况下,我们仅仅需要少量的几个组件就足够了,但是我们却将庞大的组件库打包到我们的源码中,造成了不必要的开销。

不过很多组件库已经提供了现成的解决方案,如 Element 出品的 babel-plugin-component 和 AntDesign 出品的 babel-plugin-import 安装以上插件后,在.babelrc 配置中或 babel-loader 的参数中进行设置,即可实现组件按需加载了。

单页应用的按需加载

现在很多前端项目都是通过单页应用的方式开发的,但是随着业务的不断扩展,会面临一个严峻的问题——首次加载的代码量会越来越多,影响用户的体验。

通过import(*)语句来控制加载时机,webpack 内置了对于import(*)的解析,会将import(*)中引入的模块作为一个新的入口在生成一个 chunk。 当代码执行到import(*)语句时,会去加载 Chunk 对应生成的文件。import()会返回一个Promise 对象,所以为了让浏览器支持,需要事先注入Promise polyfill.

Nginx

nginx 是一个反向代理服务器.客户端向服务端发送请求,可以配置一个 nginx 代理服务器.

反向代理

代理服务器负责接收请求,转发给内部的服务器.从颞部服务器得到响应返回给客户端.

反向代理服务器为服务端工作.对服务端是透明的,对客户端不透明.

nginx 的作用

解决跨域

请求过滤

负载均衡

配置 gzip

静态资源服务器

解决跨域

在 server 中配置一个 server-name,和客户端同域名

在 Location 中拦截向客户端域名发送的请求,并且请求代理回服务器.

请求过滤

在 Location 中过滤相应的状态码或者 url,或者请求类型

负载均衡

通过配置 upstream 指定后端服务器的地址列表

在 server 中拦截请求,并将请求转发到 upstream 的后端服务器列表里.

策略

默认采用轮训的策略,缺点是一台服务器压力过大时,会影响其他服务器.

最小连接数策略,优先分配给连接数最少的服务器

least_connt

最快响应时间策略,优先分配给响应时间最短的服务器

fair

静态资源服务器

在 Location 中配置,匹配比如说图片为结尾的请求,转发到本地路径.设置 root 指定路径.