提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
作为一个后端开发,每次碰到前端项目(多半是后台管理)时都十分头疼,每次都要从头开始学习Node、npm、webpack、vue-cli,这次索性就把入门用到的相关知识和使用示例都整理出来。
个人习惯,知识梳理的时候会按照是什么、能做什么、怎么用的思路进行整理。
一、Nodejs
1.简介(是什么)
Node.js 是一个开源、跨平台的 JavaScript 运行时环境(这是官网的介绍)。
Node.js 是一个基于 V8 JavaScript 引擎构建的 JavaScript 运行时(中文文档中的介绍)。相当于把chrome的V8引擎单独拎出来做了一个js的运行时环境。
Node.js的特点是事件驱动和异步I/O。
官网地址:https://www.nodejs.com.cn/
2.目的(能做什么)
nodejs出来以前,js代码只能在浏览器中运行,这就是js为什么被称为前端语言的原因。但是有了nodejs之后,nodejs提供了js的运行时环境,让js能够在js上运行,这样js不仅仅能实现前端功能了,也能实现很多后端功能,比如处理文件、处理数据库、处理网络请求等。由于事件机制和异步IO模型的优越性,nodejs甚至可以实现一个高性能的web服务器。nodejs还支持模块化开发,让js代码的维护、扩展、复用性更好。
总的来说,通过nodejs提供的js运行时环境和模块化功能,我们可以用js实现更多复杂的功能,也能更加方便的使用js。
3.使用(怎么用)
3.1安装
参考安装:https://blog.csdn.net/muzidigbig/article/details/80493880
3.2使用脚本模式
脚本模式就是使用node.js运行一个js脚本
首先,新建一个js文件hello.js,内容如下
console.log("Hello World");
然后通过 node命令来执行:
node helloworld.js
程序执行后,正常的话,就会在终端输出 Hello World。
3.3使用交互模式
打开终端,键入node进入命令交互模式,可以输入一条js代码语句后立即执行并显示结果,例如:
$ node
> console.log('Hello World!');
Hello World!
3.4使用 Node.js 编写网络服务器
a.首先,创建一个名为 projects 的空项目文件夹
b.然后,在 projects 文件夹中创js文件并将其命名为 hello-world.js。
代码如下:
const http = require('node:http');//引入node的http模块
const hostname = '127.0.0.1';
const port = 3000;
//创建一个server,响应固定为“Hello,World”
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello, World!\n');
});
//监听端口
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
c.最后,执行node命令运行js文件。
node hello-world.js
控制台会输出以下结果
Server running at http://127.0.0.1:3000/
访问http://127.0.0.1:3000,会出现Hello, World!,表示服务器运行成功。
3.5Node.js模块化机制
Node.js采用了模块化的开发方式,使开发者能够将功能拆分为独立的模块,并通过模块间的导入和导出实现代码的复用和组织。在Node.js中,每个文件都被视为一个模块,模块可以是一个函数、对象或类等。
Node.js的模块化遵循的是CommonJS规范,使用require函数来导入其他模块,并使用module.exports或exports来导出当前模块的内容。通过这种方式,可以将变量、函数、类等封装在模块中,并在需要的地方进行导入和使用。
模块化的好处
1.代码重用:通过将功能划分为模块,可以将已经开发和测试过的模块在不同的项目中重复使用,提高代码的复用性和开发效率。
2.可维护性:模块化设计使得软件的各个部分相对独立,当需要修改或修复某个功能时,只需关注特定的模块,而无需对整个系统进行大规模的改动,简化了维护工作。
3.并行开发:模块化可以让不同的开发人员并行工作,在保证模块接口一致性的前提下,开发团队可以独立地开发、测试和调试各自负责的模块,提高开发效率。
4.可测试性:模块化设计使得单个模块的功能较小且相对独立,这使得单元测试和集成测试更容易进行,能够更准确地定位和解决问题。
5.可扩展性:通过模块化设计,可以更方便地添加新功能或替换现有功能,只需修改或增加相关模块,而不必影响整个系统的其余部分。
require加载路径的逻辑
1.对于自己创建的模块,导入时路径建议写 相对路径 ,且不能省略 ./ 和 …/
2.js 和 json 文件导入时可以不用写后缀,c/c++编写的 node 扩展文件也可以不写后缀,但是一
般用不到
3.如果导入其他类型的文件,会以 js 文件进行处理
4.如果导入的路径是个文件夹,则会 首先 检测该文件夹下 package.json 文件中 main 属性对应
的文件,如果存在则导入,反之如果文件不存在会报错。
5.如果 main 属性不存在,或者 package.json 不存在,则会尝试导入文件夹下的 index.js 和
index.json ,如果还是没找到,就会报错
6.导入 node.js 内置模块时,直接 require 模块的名字即可,无需加 ./ 和 …/
require 导入的基本流程
1.将相对路径转为绝对路径,定位目标文件
2.缓存检测
3.读取目标文件代码
4.包裹为一个函数并执行(自执行函数)。通过 arguments.callee.toString() 查看自执行函数
5.缓存模块的值
6.返回 module.exports 的值
只需要记住require返回的是module.exports 的值就可以了
参考文章
1.nodejs中文文档:https://www.nodejs.com.cn/api/synopsis.html
2.【NodeJS】——模块化:https://blog.csdn.net/weixin_50233139/article/details/131471635
二、npm
1.简介
npm 是 Node.js 官方提供的包管理工具,他已经成了 Node.js 包的标准发布平台,用于 Node.js 包的发布、传播、依赖控制。npm 提供了命令行工具,使你可以方便地下载、安装、升级、删除包,也可以让你作为开发者发布并维护包。
2.为什么用npm
在以前,前端是怎么共享代码的呢?在 GitHub 还没有兴起的年代,前端是通过网址来共享代码。比如你想使用 jQuery ,那么你点击 jQuery 网站上提供的链接就可以下 jQuery ,放到自己的网站上使用 。GItHub 兴起之后,社区中也有人使用 GitHub 的下载功能。
但是当一个项目依赖的代码越来越多,并且一个依赖还会依赖另一个依赖,从多个网址来下载再依赖到自己的项目中是很麻烦的事情,同时版本的管理也会非常麻烦。所以npm提供了一种方案来解决包依赖的问题。
1.包的作者使用 npm publish 把代码提交到 repository 上,并给包命名;
2.使用者将包名写到 package.json 里,然后运行 npm install ,npm 就会帮他们下载代码(包所依赖的其他包也会自动下载);
3.下载完的代码出现在 node_modules 目录里,就可以随意使用了。
3.npm的使用
3.1安装npm
由于node.js内置了npm,所以安装好node后就不用安装npm了。可以通过输入 “npm -v” 来测试是否成功安装。
如果版本过低,可以通过如下命令升级:
npm install npm -g
由于默认registry(类似maven的中央仓库)在国外,下载速度慢,可以使用淘宝镜像,设置如下:
如果想要更改默认的registry地址为国内的镜像源,比如使用淘宝NPM镜像,则需要运行以下命令:
npm config set registry https://registry.npmmirror.com
这会将默认的registry地址修改为https://registry.npmmirror.com。
若要确保新的配置生效,再次运行 npm config get registry 命令来获取最新的registry地址。
3.2安装包
通过package.json安装
package.json 文件其实就是对项目或者模块包的描述,里面包含许多元信息。比如项目名称,项目版本,项目执行入口文件,项目贡献者等等。
这是官方推荐的安装包的方式,使用这种方式的好处是:
1.package.json文件可以把所有的依赖包以列表的形式展示出来,一目了然
2.可以使用semantic versioning rules这种语法来指定依赖包的版本
3.能让你的项目构建具有可复制性,更容易分享个其他开发者
认识package.json
{
"main": "index.js",#默认的入口文件
"dependencies": {},#生产环境依赖
"devDependencies": {},#本地开发和测试环境依赖
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "webpack"
},
}
main字段:当你在代码中使用 require() 函数导入一个模块时,Node.js 会根据 package.json 文件中的 main 字段来确定该模块的入口文件。例如,如果你引入了一个名为 myModule 的模块,并且myModule模块的package.json文件中的 main 字段设置为 index.js,那么当其他模块使用 require(‘myModule’) 导入 myModule 时,实际上会执行 index.js 文件中的代码。
dependencies和和devDependencies字段:dependencies是生产环境依赖,build打包时会打到生产包里去,devDependencies是本地开发和测试环境依赖,本地运行时会依赖到,但是build打包时不会打到生产包里
scripts字段:用于执行npm script,后续讲webpack的例子时会明白了
具体的安装方式如下:
1.执行npm init命令
npm init
这时会出现一些问答,比如给你的项目命名、项目描述、作者信息等等,如果不想设置,一路回车就可以了,完事后会在你执行命令的目录下生成一个package.json文件
也可以通过npm init --yes或者npm init -y命令跳过问答,这样会生成一个默认的package.json文件,如下
{
"name": "npm-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",#默认的入口文件
"dependencies": {},#生产环境依赖
"devDependencies": {},#本地开发和测试环境依赖
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
2.在dependencies对象中加入你想要依赖的包和版本
前略
"dependencies": {
"express": "^4.19.1",
"webpack": "^5.91.0"
},
后略
3.执行npm install命令
npm install
这样npm就会自动帮你下载你要依赖的包,并放在node_modules目录里
命令行安装
1.本地安装
一般会看到3种安装命令,如下
npm install <Module Name>
npm install <Module Name> --save
npm install <Module Name> --save-dev //仅开发环境依赖,生产环境不依赖
npm install --save命令就是指安装包并且把包名和版本保存到package.json文件的dependencies对象中,npm install --save-dev命令就是指安装包并且保存到package.json文件的devDependencies对象中。显然,这两个命令需要package.json文件,如果没有,执行后就会提示找不到文件,如下(但是包还是安装成功了,只是没有把包名和版本保存到package.json文件的dependencies或devDependencies对象中)
最后,npm install 命令就是安装包到node_modules目录里。另外也可以通过npm install @版本号 来指定安装的版本。
2.全局安装
如果你想将其作为一个命令行工具,那么你应该将其安装到全局。这种安装方式后可以让你在任何目录下使用这个包。比如 grunt 就应该以这种方式安装。
npm install -g <Module Name>
这种安装方式会安装到全局目录%USERPROFILE%\AppData\Roaming\npm\node_modules下
3.更新和卸载
本地更新:
在 package.json 文件所在的目录中执行 npm update 命令
本地删除:
如需删除 node_modules 目录下面的包(package),请执行:
npm uninstall 命令,如果需要从 package.json 文件中删除依赖,需要在命令后添加参数 --save:npm uninstall --save
3.3发布包
1.首先,要注册npm用户
登录官网https://www.npmjs.com/根据提示注册
2.使用命令行登录(因为需要用命令行发布)
npm login
根据提示输入用户名、密码、邮箱验证码就登录成功了
(这里要注意的是,前面使用官网注册的话,需要使用默认的registry地址。如果前面已经改为https://registry.npmmirror.com,这里需要再改回https://registry.npmjs.org/。)
3.准备需要发布的项目
执行npm init ,初始化项目名、版本、作者,生成package.json,再创建index.js和README.md文件,如下:
{
"name": "ademobyzcl",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "zhchling86",
"license": "ISC"
}
index.js内容如下(按中文手册):
exports.printMsg = function() {
console.log("This is a message from the demo package");
}
README.md随便描述一下即可
4.发布
执行npm publish,即可发布成功了
5.使用自己发布的包
新建一个项目,依赖刚刚发布的包ademobyzcl
{
"name": "npm-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"ademobyzcl": "^1.0.0"
},
"keywords": [],
"author": "",
"license": "ISC"
}
执行npm install,npm-demo项目的node_modules目录下就下载好ademobyzcl包啦
下载好后怎么使用呢?按中文手册中所述,使用require()来引入module,那什么是module呢?
A folder with a package.json file containing a main field.//一个含有package.json的文件夹,且package.json中含有main字段,也就是入口
A folder with an index.js file in it.//一个含有index.js的文件夹(与第一种类似,因为index.js就是默认的入口)
A JavaScript file.//一个js文件
显然,我们的ademobyzcl包属于第一种
那么我们在项目中新建一个js文件main.js,引入ademobyzcl包,如下:
const ademo = require('ademobyzcl')
ademo.printMsg();
在main.js所在文件目录下执行一下命令,就可以运行了
node main.js
执行结果如下:
ademobyzcl包中的log就打印出来啦
另外,也可以使用npm命令来执行main.js,需要在package.json中添加script,如下
…
“scripts”: {
“dev”:“node main.js”,
“test”: “echo “Error: no test specified” && exit 1”
},
…
再执行npm run dev,同样ademobyzcl包中的log也打印出来啦
参考文档:npm中文手册https://www.npmjs.cn/getting-started/using-a-package.json/
三、webpack
1.简介
前面讲nodejs和npm都是针对的js,还没涉及到html、css和浏览器,而webpack就是用来给项目打包的一个工具。按官网文档介绍,本质上,webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。
2.目的
在单体web项目中,我们把静态代码和java代码一起打包成war后部署在tomcat上。而前后端分离后,前端工程需要单独进行打包、部署,此时便需要打包工具了。另外我们想使用更加高级的语法时(比如less和sass),想使用模块化,而浏览器不支持怎么办,又需要进行转化,这也离不开打包工具。因此,webpack打包工具有如下作用:
1.打包:将所有模块打包构建成静态资源,相比于通过script标签,script标签引入是下载全部的js,而webpack打包只会将用到的js打包进来。
2.代码压缩:将JS、CSS代码混淆压缩,让代码体积更小,加载更快
3.编译语法:编写CSS时使用Less、Sass,编写JS时使用ES6、TypeScript等,这些标准目前都无法被浏览器兼容,因此需要构建工具编译,例如使用Babel编译ES6语法。
4.处理模块化:CSS和JS的模块化语法,目前都无法被浏览器兼容。因此开发环境可以使用既定的模块化语法,但是需要构建工具将模块化语法编译为浏览器可识别形式。
(ps:其他的构建工具:最早普及使用的是Grunt,后面又出现Gulp。Webpack是目前流行的构建工具)
webpack模块化与nodejs模块化的区别(个人理解)
前面介绍过,nodejs模块化是基于common.js规范的,使用require函数来导入其他模块,并使用module.exports或exports来导出当前模块的内容。
webpack是在nodejs环境下的打包工具,显然nodejs模块化方式webpack也能用,而webpack还支持ES6的模块化方式,及通过export和import来导出和导入js
3.安装
本地安装
首先创建进入一个目录并初始化 npm,然后在本地安装 webpack和webpack-cli,-D就是–save-dev的简写,即保存到package.json的devDepencies对象中
mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli -D
安装完后执行.\node_modules.bin\webpack -v(因为是本地安装,需要加上本地安装的路径),查看是否安装成功(Node 8.2/npm 5.2.0 及以上版本提供的 npx 命令,可以运行在最初安装的 webpack 包中的 webpack 二进制文件npx webpack -v)
4.webpack使用(无配置)
不使用webpack,通过 script 脚本引入
现在创建以下目录结构、文件和内容:
webpack-demo
|- package.json //npm init -y后会生成
|- package-lock.json //安装完webpack后会生成
|- index.html
|- /src
|- index.js
src/index.js内容:
function component() {
const element = document.createElement('div');
// 执行这一行需要引入 lodash(目前通过 script 脚本引入,假定_作为全局变量)
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
index.html内容:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>起步</title>
<script src="https://unpkg.com/lodash@4.17.20"></script>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>
用浏览器打开index.html,显示Hello webpack
在此示例中,
通过 webpack构建
首先,本地安装ladash
npm install --save lodash
然后显示的引入lodash
import _ from 'lodash';
function component() {
const element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
执行webpack命令,此时会默认从./src/index.js开始进行打包,生成dist目录,dist目录下有main.js,就是打包后的js
npx webpack
最后修改index.html重新引入main.js
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>起步</title>
</head>
<body>
<script src="./dist/main.js"></script>
</body>
</html>
用浏览器打开index.html,也会显示Hello webpack
另外,可以通过npm scripts来简化构建的执行,在package.json的scripts对象中添加"build": “webpack”,使用 npm scripts 便可以像使用 npx 那样通过模块名引用本地安装的 npm 包
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
...
这样,直接运行npm run build就可以完成构建了
5.webpack使用(有配置)
上面介绍了webpack可以不通过配置运行,然而大多数项目会需要很复杂的设置,因此 webpack 仍然支持配置文件,这比在终端中手动输入大量命令更加高效。
基本配置
entry:表示 webpack 的入口,指定打包的文件,可以一个或多个。
output:表示输出文件的路径和文件名。
mode:表示打包环境,可设置 production 或 development 值。
在项目目录下创建文件webpack.config.js:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
mode: 'development'
};
上面配置会解析 src 下的 app.js 文件的相关依赖并打包,然后输出到当前项目的 dist 文件夹下,输出文件名为 main.js。
output 中的 clean 会在打包生成文件之前会按照这个字段的值进行清理行为,如果为 true,则清空 output 设定的目录下的文件。
模式为开发模式,开发模式可以编译 ES Module语法,而生成模式(production)则不仅可以编译 ES Module语法,还可以压缩代码。
loader
webpack 的 Loader 主要用于对模块的转换,也就是对文件的转化,如处理CSS,将 Less、Sass 处理成 CSS,将图片处理成 data URL 等。
下面以处理CSS为例介绍,其他处理如图片、字体等详见官网文档
1.安装 style-loader 和 css-loader,并在 module 配置 中添加这些 loader:
npm i css-loader style-loader -D
2.新建webpack.config.js文件:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
};
注意:module loader 可以链式调用。链中的每个 loader 都将对资源进行转换,不过链会逆序执行。第一个 loader 将其结果(被转换后的资源)传递给下一个 loader,依此类推。最后,webpack 期望链中的最后的 loader 返回 JavaScript。
请确保 loader 的先后顺序:‘style-loader’ 在前,而 ‘css-loader’ 在后。如果不遵守此约定,webpack 可能会抛出错误。
3.在src目录下新建 style.css 文件
.hello {
color: red;
}
4.修改src/index.js:
import _ from 'lodash';
import './style.css';
function component() {
const element = document.createElement('div');
// lodash 现在使用 import 引入。
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');
return element;
}
document.body.appendChild(component());
在package.json的scripts字段中增加build命令
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config webpack.config.js"
},
运行npm run build命令,就会将js和css打包成dist/main.js了(如果–config webpack.config.js不加默认就是找webpack.config.js配置)
用浏览器打开index.html,也会显示红色的Hello webpack啦
plugin
webpack 的插件能够增强 webapck 的功能,比如提取 CSS 为单独文件、自动引入 JS 和 CSS、压缩 CSS。
html-webpack-plugin
最常用的是自动引入 JS 和 CSS
打包后,不管是 JS 还是 CSS 打包为单独文件时,在 index.html 中都需要手动引入相应的资源,如果修改了 webpack 打包 JS 和 CSS 文件的路径,那么 index.html 也需要修改。
通过 HtmlWebpackPlugin 插件,自动生成index.html 并引入JS和CSS,便能解决上述问题
首先安装插件:
npm i html-webpack-plugin -D
然后修改 webpack.config.js 配置:
...
const HtmlWebpackPlugin = require('html-webpack-plugin');
...
plugins: [
new HtmlWebpackPlugin({
title: '管理输出',
template: path.resolve(_dirname,'index.html')
}),
],
template的作用是将 index.html作为模板再引入JS和CSS,否则生成的index.html就只有js和css,没有其他内容
打包后,可看到在 dist 下有一个 index.html
copy-webpack-plugin
有一些静态资源文件。不通过webpack打包。而是手动复制到打包文件里面。这时候,就需要利用这个plugin来帮助我们自动复制。
这个功能也很常用,后面介绍vue-cli时就有使用
首先安装:
npm install --save-dev copy-webpack-plugin
常用 API:
1.patterns:一个数组,包含要复制的源文件和目标文件的信息。每个数组元素都是一个对象,其中包含以下属性:
from:源文件路径或模式。可以是字符串或正则表达式。
to:目标文件路径。可以是字符串或函数。默认是compiler.options.output,也就是打包输出的路径
toType:目标文件类型。可以是 'file' 或 'dir'。默认值是 'file'。
flatten:是否将源文件复制到目标文件的子目录中。默认值是 false。
transform:一个函数,用于在复制文件之前对源文件进行转换。
noErrorOnMissing:为ture表示找不到目标文件时不会报错
2.options:一个对象,包含一些全局选项,如 concurrency(并发复制的文件数)和 overwrite(是否覆盖已存在的文件)。
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
plugins: [
new CopyPlugin({
patterns: [{
from: 'src/index.html',
to: 'dist/index.html'
},
{
from: 'src/assets',
to: 'dist/assets'
},
{
from: 'src/images',
to: 'dist/images',
toType: 'dir'
},
{
from: 'src/styles.css',
to: 'dist/styles.css',
transform: (content, path) => minifyCSS(content)
},
{
from: '**/*',
to: './',
globOptions: {
ignore: ['**/*.js', '**/*.scss', '**/*.ts']
}
},
{
from: "public/**/*",
filter: async (resourcePath) => {
const data = await fs.promises.readFile(resourcePath);
const content = data.toString();
if (content === "my-custom-content") {
return false;
}
return true;
},
},
],
options: {
concurrency: 10,
overwrite: true
}
})
]
};
在这个示例中,我们创建了一个 CopyWebpackPlugin 实例,并指定了要复制的文件和目录。第一个模式将 src/index.html 文件复制到 dist/index.html 文件,第二个模式将 src/assets 目录复制到 dist/assets 目录,第三个模式将 src/images 目录复制到 dist/images 目录,第四个模式将 src/styles.css 文件复制到 dist/styles.css 文件,并在复制之前对内容进行转换(例如,压缩 CSS)。我们还指定了一些全局选项,如并发复制的文件数和是否覆盖已存在的文件。
参考文章:https://blog.csdn.net/weixin_43972437/article/details/132888267
使用 source map
当 webpack 打包源代码时,可能会很难追踪到错误和警告在源代码中的原始位置。例如,如果将三个源文件(a.js,b.js 和 c.js)打包到一个 bundle(bundle.js)中,而其中一个源文件包含错误,那么堆栈跟踪就会直接指向到 bundle.js,却无法准确知道错误来自于哪个源文件,所以这种提示通常无法提供太多帮助。
为了更容易地追踪错误与警告在源代码中的原始位置,JavaScript 提供了 source map 功能,可以帮助将编译后的代码映射回原始源代码。source map 会直接告诉开发者错误来源于哪一个源代码。
修改webpack.config.js配置
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
devtool: 'inline-source-map', #新增source-map配置
...
}
devServer
devserver 可以在本地启动一个服务,使我们可以使用 http 协议去访问 index.html ,并且当修改代码时,能够监听当前项目的文件变动从而实现自动打包,而不用每次修改后都要手动打包:
安装webpack-dev-server,这里要特别注意,如果webpack和webpack-cli安装的是5.X,那么webpack-dev-server一定要安装4.X,我因为安装成5.X导致报错[webpack-cli] Error: Not supported,找了很久的原因才发现是版本的问题。
npm i webpack-dev-server@4.15.2 -D
修改webpack.config.js配置文件,告诉 dev server 应从什么位置开始查找文件:
...
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/resource',
},
devtool: 'inline-source-map',
#新增devserver的配置
devServer: {
static: './public',
host: 'localhost',
port: 8080,
open: true
},
...
}
配置解读
前提知识:webpack构建时会从entry打包到output的path.filename中,而webpack-dev-server不会打包成文件,而是打包后放在内存中,部署到http://[devServer.host]:[devServer.port]上,可以通过http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename] 进行访问。例如上面的配置,可以通过localhost:8080/resource/main.js访问打包后的js(事实上,项目根本没有/resource/main.js文件,因为是在内存中)。此时./public目录下的html引用main.js的路径为/resource/main.js
<script src="/resource/main.js"></script>
static: ‘./public’:告知 webpack-dev-server 将./public目录(static不配置默认也是./public)下的文件作为可访问的静态资源部署在 localhost:8080,localhost:8080默认是访问index.html。./public目录下所有文件都可以访问。
host: ‘localhost’:ip为本机
port: 8080:端口为8080
open: true:部署后在浏览器打开
运行以下命令,即可部署并打开浏览器页面了(–config是指定配置文件,默认就是webpack.config.js)
npx webpack serve --config webpack.config.js
参考文档:
webpack中文文档:https://www.webpackjs.com/guides/getting-started
webpack配置文档:https://www.webpackjs.com/configuration/dev-server/
Webpack入门,这一篇就够了:https://zhuanlan.zhihu.com/p/614835591
四、vue-cli
1.简介
在开发中,需要打包的东西不止是js、css、html。还有更多的东西要处理,这些插件和加载器如果我们一一去添加就会比较麻烦。幸好,vue官方提供了一个快速搭建vue项目的脚手架:vue-cli。使用它能快速的构建一个web工程模板。
2.安装
npm install -g vue-cli
3.使用
1)新建一个文件夹vuecli-demo
2)执行下面命令,快速搭建一个webpack的项目:
vue init webpack
一路回车,vue-rooter选择yes,ESlint和test选择no
创建过程参数说明:
Project name :项目名称----不能有大写字母
Project description:项目描述
Author:作者
Vue build:如何构建项目
Install vue-router:是否安装路由
Use ESLint to lint your code:是否使用ESLint来规范我们的代码
Pick an ESLint preset:选择一个ESLint代码规范
Set up unit tests:是否需要自动化单元测试
Setup e2e tests with Nightwatch:是否需要自动化用户界面测试
Should we run ‘npm install’ for your after the project has been created?(recommend):在后续安装依赖包是是否使用npm install安装
3)执行npm run dev,即可启动项目,访问localhost:8080,即可出现下面页面
执行npm run build命令,构建项目,打包到dist目录
4)项目结构说明
build:项目webpack配置文件
config:针对开发环境和线上环境的配置文件
node_modules:项目依赖包
src:源代码目录
static:静态资源
.babelrc:JavaScript 语法的编译器
.editorconfig:针对babel的编译,浏览器配置
.eslintignore:针对babel的编译,eslint检测规则配置
.eslintrc.js:针对babel的编译,eslint检测规则配置
.gitignore:git 配置文件
.postcssrc.js:转换成css格式的插件
index.html:整个项目的入口index页,包含根实例的挂载点
package.json:定义了这个项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等元数据)
package-lock.json:其实package-lock就是锁定安装时的包版本号,需要上传到git上,以保证其他人在install时候,大家的依赖版本相同
5)主要配置文件说明
package.json
npm run dev命令就是执行webpack-dev-server --inline --progress --config build/webpack.dev.conf.js。
–line:默认是true, 如果开启inline, DevServer会在构建完变化后的代码时通过代理客户端控制网页刷新,也就是自动刷新网页
–progress:显示部署的百分比进度
webpack.dev.conf.js
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool,
// these devServer options should be customized in /config/index.js
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
hot: true,
//contentBase主要作用是如果我们打包后的资源,又依赖于其他的一些资源,那么就需要指定从哪里来查找这个内容,这里因为CopyWebpackPlugin会复制,所以不需要配置。
contentBase: false, // since we use CopyWebpackPlugin.
compress: true,
host: HOST || config.dev.host,
port: PORT || config.dev.port,
open: config.dev.autoOpenBrowser,
overlay: config.dev.errorOverlay
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath,
proxy: config.dev.proxyTable,
quiet: true, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
//自动生成index.html
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
// copy custom static assets
//从static目录
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
首先,将baseWebpackConfig中的配置进行了合并
然后,配置了devServer,配置了HtmlWebpackPlugin和CopyWebpackPlugin插件,这些前面都讲过。
看一下baseWebpackConfig
...
module.exports = {
//entry的上下文路径,当前文件目录是build目录,'../'就是当前根目录
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
output: {
//打包路径 '../dist'
path: config.build.assetsRoot,
//打包生成的文件名
filename: '[name].js',
//打包的公共路径'/'
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
/*resolve用来配置模块如何解析。例如,当在 ES2015 中调用 import 'lodash',resolve 选项能够
对 webpack 查找 'lodash' 的方式去做修改*/
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
//这样导入vue/dist/vue.esm.js时,可以简化为import xx from vue,其中$表示精准匹配
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
//vue-loader解析.vue文件
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
...
]
}
build.js
const spinner = ora('building for production...')
spinner.start()
//删除path.join的目录,也就是/dist/static目录,如果删除成功,使用webpackConfig进行打包,webpackConfig就是webpack.prod.config.js输出的配置
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})
6.其他文件说明
先看index.html,里面就有一个
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>vuecli-demo</title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
再来看入口文件./src/main.js,它是在webpack.base.config.js中配置的
import Vue from 'vue'
import App from './App' //引入了./App.vue
import router from './router' //引入了./router/index.js
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',//要渲染的的页面元素
router, // 引用上面的router对象
components: { App },//在Vue实例中定义局部组件App
template: '<App/>' //在html页面中添加<App></App>模板,也就是使用局部组件App
})
使用局部组件也有一种简化的写法
new Vue({
el: '#root',
render: h => h(App)
})
//相当于
new Vue({
el: '#root',
template: '<App></App>',
components: {
App
}
})
再来看一下App.vue文件,它其实就是一个组件,因为它里面有
<template>
<div id="app">
<img src="./assets/logo.png">
<router-view/> <!--这里是vue-router的锚点,会自动将路径匹配的路由渲染到这里-->
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
继续看一下./src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
Vue.use(Router) //这里必须要有,否则router不生效
export default new Router({
//定义路由的配置,当路径为'/'时将HelloWorld组件渲染进来
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
}
]
})
最后总结一下,首先Vue实例中定义了要渲染的标签,即id为app的标签,使用了局部组件app,局部组件中定义了一个img标签和路由的锚点,路由的路径为’/'时将HelloWorld组件渲染进来。所以,访问localhost:8080时,页面先展示了一张图片,再展示了HelloWorld组件中的内容。
参考文章:
Vue项目结构分析:https://blog.csdn.net/u010412833/article/details/112626070
4.@vue/cli
上述的vue-cli的版本是2.9.6,而vue-cli 3以后的版本在命令和脚本使用上与vue-cli 2有很大不同,下面简单介绍一下@vue/cli的使用
4.1 安装
#如果全局安装了vue-cli,要先npm uninstall vue-cli -g全局卸载掉后,再全局安装@vue-cli
npm install @vue/cli -g
安装完使用下面命令检查一下,注意V是大写
vue -V
4.2 使用
执行下面创建项目的命令
npx vue create vuecli3-demo
此时会不断出现选择提示
a.选择Manually select features,按回车
b. 选择babel、Router、Vuex(通过按空格键选择或者取消),按回车
c.选择2.x,按回车
d.Use history mode for router?输入y;
Where do you prefer placing…?选择package.json;
Save this as a preset for future projects?输入n(如果选y,表示把当前配置保存为preset预配置,后续创建project时可以直接使用这个preset,就不用一步步设置了)
配置完后按回车就开始安装了,安装完后进入项目根目录,执行命令npm run serve
cd vuecli3-demo
npm run serve
出现以下画面
4.3 配置文件解读
相比于vue-cli,@vue/cli的配置文件有了些许不同,并且,vue create构建出来的脚手架项目里的配置文件没有vue-cli清晰易读,本着打破砂锅问到底的精神,深挖一下配置,不感兴趣的可以跳过,有错误的地方也请指出
先看package.json
...
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
...
可以看出,执行npm run serve时,实际上执行的是vue-cli-service serve命令
再看vue-cli-service
打开.\node_modules.bin目录下的vue-cli-service文件
#!/bin/sh
#获取当前文件所在目录
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
esac
#如果当前路径有node,也就是本地安装了,使用本地的node执行,否则使用全局的node执行命令
if [ -x "$basedir/node" ]; then
"$basedir/node" "$basedir/../@vue/cli-service/bin/vue-cli-service.js" "$@"
ret=$?
else
node "$basedir/../@vue/cli-service/bin/vue-cli-service.js" "$@"
ret=$?
fi
exit $ret
可以看出,最终使用node执行了…@vue\cli-service\bin下的vue-cli-service.js文件
再看vue-cli-service.js
//引入并创建了Service实例
const Service = require('../lib/Service')
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
const rawArgv = process.argv.slice(2)
const args = require('minimist')(rawArgv, {
boolean: [
// build
// FIXME: --no-module, --no-unsafe-inline, no-clean, etc.
'modern',
'report',
'report-json',
'inline-vue',
'watch',
// serve
'open',
'copy',
'https',
// inspect
'verbose'
]
})
const command = args._[0]
//执行了service的run方法
service.run(command, args, rawArgv).catch(err => {
error(err)
process.exit(1)
})
可以看到,首先引入并创建了Service实例,然后执行了Service的run方法,所以我们需要看Service的run方法,看方法前,先看下Service的构造
Service的构造
constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
process.VUE_CLI_SERVICE = this
this.initialized = false
this.context = context
this.inlineOptions = inlineOptions
this.webpackChainFns = []
this.webpackRawConfigFns = []
this.devServerConfigFns = []
this.commands = {}
// Folder containing the target package.json for plugins
this.pkgContext = context
// package.json containing the plugins
//解析package.json中的配置,比如入口main,plugins等
this.pkg = this.resolvePkg(pkg)
// If there are inline plugins, they will be used instead of those
// found in package.json.
// When useBuiltIn === false, built-in plugins are disabled. This is mostly
// for testing.
//解析plugins
this.plugins = this.resolvePlugins(plugins, useBuiltIn)
// pluginsToSkip will be populated during run()
this.pluginsToSkip = new Set()
// resolve the default mode to use for each command
// this is provided by plugins as module.exports.defaultModes
// so we can get the information without actually applying the plugin.
this.modes = this.plugins.reduce((modes, { apply: { defaultModes } }) => {
return Object.assign(modes, defaultModes)
}, {})
}
可以看出,主要的是解析package.json和里面的plugins,看下解析plugins的方法
resolvePlugins (inlinePlugins, useBuiltIn) {
const idToPlugin = (id, absolutePath) => ({
id: id.replace(/^.\//, 'built-in:'),
apply: require(absolutePath || id)
})
let plugins
//默认的plugins
const builtInPlugins = [
'./commands/serve',
'./commands/build',
'./commands/inspect',
'./commands/help',
// config plugins are order sensitive
'./config/base',
'./config/assets',
'./config/css',
'./config/prod',
'./config/app'
].map((id) => idToPlugin(id))
...
// Local plugins
//pakage.json中定义的vuePlugins
if (this.pkg.vuePlugins && this.pkg.vuePlugins.service) {
const files = this.pkg.vuePlugins.service
if (!Array.isArray(files)) {
throw new Error(`Invalid type for option 'vuePlugins.service', expected 'array' but got ${typeof files}.`)
}
plugins = plugins.concat(files.map(file => ({
id: `local:${file}`,
apply: loadModule(`./${file}`, this.pkgContext)
})))
}
...
//排序
const orderedPlugins = sortPlugins(plugins)
...
return orderedPlugins
可以看到,定义了默认的plugins,也解析了pakage.json中定义的vuePlugins,最后做了排序。最终的plugins是一个由map组成的list,map的key是id,value是apply函数,如下
const idToPlugin = (id, absolutePath) => ({
id: id.replace(/^.\//, 'built-in:'),
apply: require(absolutePath || id)
})
以默认的’./commands/serve’为例,id是’./commands/serve’, value是引入的./commands/serve.js中方法(下面会分析如何调用)
继续看Service.run方法
async run (name, args = {}, rawArgv = []) {
// resolve mode
// prioritize inline --mode
// fallback to resolved default modes from plugins or development if --watch is defined
const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
// --skip-plugins arg may have plugins that should be skipped during init()
this.setPluginsToSkip(args, rawArgv)
// load env variables, load user config, apply plugins
//加载env变量,加载用户定义的config,执行plugins
await this.init(mode)
args._ = args._ || []
let command = this.commands[name]
if (!command && name) {
error(`command "${name}" does not exist.`)
process.exit(1)
}
if (!command || args.help || args.h) {
command = this.commands.help
} else {
args._.shift() // remove command itself
rawArgv.shift()
}
const { fn } = command
return fn(args, rawArgv)
}
可以看到主要执行的是this.init(mode)方法
继续看Service的init方法
init (mode = process.env.VUE_CLI_MODE) {
if (this.initialized) {
return
}
this.initialized = true
this.mode = mode
// load mode .env
if (mode) {
this.loadEnv(mode)
}
// load base .env
this.loadEnv()
// load user config
//加载用户自己配置的./vue.config.js
const userOptions = this.loadUserOptions()
const loadedCallback = (loadedUserOptions) => {
this.projectOptions = defaultsDeep(loadedUserOptions, defaults())
debug('vue:project-config')(this.projectOptions)
// apply plugins.
//执行plugins
this.plugins.forEach(({ id, apply }) => {
if (this.pluginsToSkip.has(id)) return
apply(new PluginAPI(id, this), this.projectOptions)
})
// apply webpack configs from project config file
if (this.projectOptions.chainWebpack) {
this.webpackChainFns.push(this.projectOptions.chainWebpack)
}
if (this.projectOptions.configureWebpack) {
this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
}
}
if (isPromise(userOptions)) {
return userOptions.then(loadedCallback)
} else {
return loadedCallback(userOptions)
}
}
可以看到,主要干了两件事,一是加载用户自己配置的vue.config.js,二是执行plugins
首先加载用户定义的config,一般就是./vue.config.js,为什么是./vue.config.js呢?
loadUserOptions () {
const { fileConfig, fileConfigPath } = loadFileConfig(this.context)
if (isPromise(fileConfig)) {
...
}))
}
loadUserOptions 方法调用了@vue/cli-service/lib/util/loadFileConfig.js的loadFileConfig方法
module.exports = function loadFileConfig (context) {
let fileConfig, fileConfigPath
//定义了3个文件路径
const possibleConfigPaths = [
process.env.VUE_CLI_SERVICE_CONFIG_PATH,
'./vue.config.js',
'./vue.config.cjs',
'./vue.config.mjs'
]
//遍历上面3个文件路径,取最先找到的那个,所以说一般就是./vue.config.js
for (const p of possibleConfigPaths) {
const resolvedPath = p && path.resolve(context, p)
if (resolvedPath && fs.existsSync(resolvedPath)) {
fileConfigPath = resolvedPath
break
}
}
//解析./vue.config.js
if (fileConfigPath) {
const { esm } = isFileEsm.sync(fileConfigPath)
if (esm) {
fileConfig = import(pathToFileURL(fileConfigPath))
} else {
fileConfig = loadModule(fileConfigPath, context)
}
}
return {
fileConfig,
fileConfigPath
}
}
module.exports = function loadFileConfig (context) {
let fileConfig, fileConfigPath
const possibleConfigPaths = [
process.env.VUE_CLI_SERVICE_CONFIG_PATH,
'./vue.config.js',
'./vue.config.cjs',
'./vue.config.mjs'
]
然后执行plugins,就是循环plugins这个list中map的apply方法,以上文plugins中默认的’./commands/serve’为例,也就是调用./commands/serve.js中方法
最后看./commands/serve.js中方法
//api就是上文中的Service对象
module.exports = (api, options) => {
const baseUrl = getBaseUrl(options)
//向Service中注册了serve命令、参数和对应的执行函数(所以我们才能使用vue-cli-service serve命令来启动)
api.registerCommand('serve', {
description: 'start development server',
usage: 'vue-cli-service serve [options] [entry]',
options: {
'--open': `open browser on server start`,
'--copy': `copy url to clipboard on server start`,
'--stdin': `close when stdin ends`,
'--mode': `specify env mode (default: development)`,
'--host': `specify host (default: ${defaults.host})`,
'--port': `specify port (default: ${defaults.port})`,
'--https': `use https (default: ${defaults.https})`,
'--public': `specify the public network URL for the HMR client`,
'--skip-plugins': `comma-separated list of plugin names to skip for this run`
}
}, async function serve (args) {
info('Starting development server...')
...
//引入了WebpackDevServer
const WebpackDevServer = require('webpack-dev-server')
...
// resolve webpack config
//解析webpack的config,都是一些基础配置,不是用户需要关注的
const webpackConfig = api.resolveWebpackConfig()
...
//加载用户通过vue.config.js配置的config,比如配置devServer:{port:8080}
const projectDevServerOptions = Object.assign(
webpackConfig.devServer || {},
options.devServer
)
...
//解析最终的options,这里args.host就是用命令行传入的options,比如执行vue-cli-service serve --host --port,projectDevServerOptions.host就是上面vue.config.js配置devServer中的,可以看出,命令行配置优先于vue.config.js配置
const host = args.host || process.env.HOST || projectDevServerOptions.host || defaults.host
portfinder.basePort = args.port || process.env.PORT || projectDevServerOptions.port || defaults.port
const port = await portfinder.getPortPromise()
...
// create server
//创建server
const server = new WebpackDevServer(Object.assign({
},...)
...
return new Promise((resolve, reject) => {
...
//启动server
server.start().catch(err => reject(err))
})
serve.js中方法主要是加载配置,创建server和启动server,其中配置的优先顺序是命令行参数>vue.config.js配置的devServer参数,比如在vue.config.js中配置devServer:{port:8080},在启动时执行npx vue-cli-service serve --port 8081,最终服务的端口为8081。
另外,server就是对webpackDevServer的封装,因此vue.config.js的devServer配置与webpack的配置文件webpack.config.js的devServer一样,其他配置也基本类似。
module.exports = {
productionSourceMap: false,//生产环境是否要生成 sourceMap
publicPath: './',//部署应用包时的baseURL,用法和 webpack 本身的 output.publicPath 一致
outputDir: 'dist',//build 时输出的文件目录
assetsDir: 'assets',//放置静态文件夹目录
devServer: {
port: 8090,
host: '0.0.0.0',
https: false, //是否启用 https
open: true //启动时是否直接打开浏览器
},
// 其他配置
...
至此@vue/cli的配置就分析完了
小结:@vue/cli的npm run serve命令实际上是执行vue-cli-service serve命令,vue-cli-service命令是执行vue-cli-service.js,创建了Service实例,Service实例调用了init方法,加载了vue.config.js配置文件,加载了’./commands/serve’, ‘./commands/build’,‘./commands/inspect’,'./commands/help’等plugin文件,注册了serve、build、inspect、help等命令,所以如果vue-cli-service serve命令会调用./commands/serve.js,创建并启动server,完成了服务的启动