# my-vue-cli **Repository Path**: coder_chenjun/my-vue-cli ## Basic Information - **Project Name**: my-vue-cli - **Description**: 此仓库讲述了怎么利用webpack构建一个spa以及达成类似vue-cli创建的工程化项目的效果,以便更好的理解工程化项目的来龙去脉 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 0 - **Created**: 2023-12-20 - **Last Updated**: 2024-10-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 简介 > 此文档写于2023-12-19,使用的node的版本是v18.16.1,npm安装的版本是9.5.1,各种安装的组件都没有指定特定的版本号,你照着操作出现错误,可能是一些库之间不兼容,为了避免解决这些无聊的兼容性问题,你可以安装跟我一样的node版本 前端工程化,就是**降本提效**的体现,广义上,前端工程化包含一切以**降低成本、提高效率、保障质量**为目的的手段 通过一系列的规范、流程、工具达到**研发提效、自动化、保障质量、服务稳定、预警监控**等 在https://www.zhihu.com/question/433854153里有列出工程化开发中牵涉到各种技术目标及实现 # nodejs Node.js 是一个开源的、跨平台的 JavaScript 运行时环境。Node.js 是 C++ 开发的,是一个基于 Chrome V8 引擎的 JavaScript 运行环境。 Node.js 的诞生带给人们的是个大大的惊喜,传统上 Web 开发者,前端用 JS 写,但是写服务器端代码的时候还必须用另外一种语言,类似 Ruby/Java/PHP 等。但是 Node.js 出现之后,JS 前后通吃了 ## 安装下载 nodejs的官方网址是https://nodejs.org/en,首页就有显目的最新版本的nodejs下载按钮,如果你想下载其它平台或其它版本就点击首页最新版本按钮下的`Other Downloads`链接,在其它下载页面里点击`Previous Releases`链接就可以找到其它的版本下载。 现在下载的node程序中,自带有npm这个包管理工具,它是不需要额外单独下载的,安装好了node也就相当于安装好了npm这个包管理工具 安装好之后,可以在终端中输入下面的命令了解版本信息 ```powershell node -v npm -v ``` ## HelloWorld 安装nodejs之后,编写下面的js文件 ```js console.log('hello') ``` 直接在此js文件目录下执行如下命令来执行此js文件 ```shell node hello.js ``` 运行输出的结果是 ```shell hello ``` Node.js 中的全局对象是 `global`, 类似于浏览器中的`window`,此全局对象是有console这个属性成员的 > 思考一下,js代码改为alert('hello'),执行时为什么报错? ## 简单的web服务器 编写下面的js代码 ```js const http = require('http') const hostname = '127.0.0.1' const port = 3000 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}/`) }) ``` 运行之后就可以在浏览器访问地址http://127.0.0.1:3000/来发起web请求了,响应的结果就是`hello world` ## nodejs的模块(module) 为了让Node.js的文件可以相互调用,Node.js提供了一个简单的模块系统。 模块是Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个 Node.js 文件就是一个模块,这个文件可能是JavaScript 代码、JSON 或者编译过的C/C++ 扩展。 ### 模块 `require/module.exports`属于`CommonJS`规范,`import/export`属于ES6规范。我们一起来看下其中的不同。 CommonJS规范规定,每个模块内部,`module`变量代表当前模块。这个变量是一个对象。 编写moduletest.js文件,内容如下 ```js console.log(module) ``` 执行之后,输出的结果如下 ```js Module { id: '.', path: 'D:\\tmp\\test\\src', exports: {}, filename: 'D:\\tmp\\test\\src\\moduletest.js', loaded: false, children: [], paths: [ 'D:\\tmp\\test\\src\\node_modules', 'D:\\tmp\\test\\node_modules', 'D:\\tmp\\node_modules', 'D:\\node_modules' ] } ``` `module`上有一些属性: - `id`,模块的标识符 - `exports`,模块的导出对象 - `parent`,表示当前模块的父模块,当前模块是谁加载的 - `filename`,模块的绝对路径 - `loaded`,表示是否加载完成 - `children`,表示当前模块加载了哪些模块 - `paths`,表示模块的搜索路径,路径的多少取决于目录的深度,寻找第三方模块时会用到 在模块的内部,`this`指向的是当前模块的导出对象: ```js console.log(this === module.exports); // true console.log(this === exports); // true ``` ### 创建模块与引入模块 创建一个文件,比如js文件就创建了一个模块,比如下面的hello.js文件 ```js exports.world = function() { console.log('world'); } function hello(){ console.log('hello') } ``` 其中导出的功能是world函数,hello是没有导出的,在别的模块是无法使用的 引入模块就是`require`实现的,建立`main.js`,编写下面的代码 ```js var h = require('./hello') h.world() h.hello() ``` 执行main.js时world方法正确输出,hello方法会报`h.hello is not a function`的错误 ### 模块类型 NodeJS支持三种类型的文件,`.js`、`.json`、`.node`(C++扩展二进制模块)。 模块类型分为核心模块、文件模块、第三方模块,其中核心模块是编译二进制文件,加载速度最快,比如`fs/path/http`等。 当尝试加载一个不带后缀的文件时,比如`require('./test')`,会依次尝试`test/test.js/test.json/test.node`。 ### 加载流程 通过`require`加载模块的流程: 1. 找到需要加在的模块文件 2. 判断是否缓存过,如果没有,就读取模块文件 3. 把读取到的内容放到一个自执行函数中执行 ```js (function (exports, require, module, __filename, __dirname) { //模块的代码实际上在这里 }); ``` 4. 返回`module.exports`需要导出的内容 > 模块缓存可以通过`require.cache`查看,其返回一个对象,`key`是文件绝对路径,`value`是`Module`对象。 ### webpack中模块使用 我们在用`webpack`做项目的时候,经常CommonJS和ES模块混用,这是因为`babel`做了转化,将它们都转为CommonJS。 - 转化导出 ES6的导出类型有: ```js export default 123; export const a = 123; const b = 3; const c = 4; export { b, c }; ``` `babel`会把它们转化为: ```js exports.default = 123; exports.a = 123; exports.b = 3; exports.c = 4; exports.__esModule = true; ``` 就是将ES6模块内容赋值给`exports`,然后加上`__esModule`属性。 - 转化导入 ES6的导入大概有这么几种: ```js import a from './a.js'; import * as b from './b.js'; import { c } from './c.js'; ``` 对于第一种导入方式,本意是想导入模块的`default`属性,所以会被转化为: ```js const a = require('./a.js').default; ``` 所以如果你使用了`babel`,想要`require`方法导入ES6模块的`default`输出,可以用上述方式。 第二种导入方式,就是导入`b.js`文件中所有输出,其实就是`exports`对象,所以会被转为: ```js const b = require('./b.js'); ``` 第三种方式会被转为: ```js const { c } = require('./c.js'); ``` ## npm 在Node.js 中,会将某个独立的功能封装起来,用于发布、更新、依赖管理和进行版本控制。 Nodejs 根据CommonJS规范实现了包机制,开发了NPM包管理工具,用来解决包的发布和获取需求。 **Node.js的包和模块**并没有本质的不同,包是在模块的基础上更进一步的组织JavaScript代码的目录。在https://npm.nodejs.cn/about-packages-and-modules有更详细的说明 想知道有哪些包,可以在https://www.npmjs.com/网站进行搜索,你自己也可以创建一个自己的node包,并上传到此服务器上,供全世界其它用户搜索使用,创建并发布包的方法见https://juejin.cn/post/6987695534504935438 ### init 通常创建一个node项目或者说包程序都是先创建一个文件夹,并在此文件夹里执行命令`npm init`起步的 命令`npm init`用来创建一个 package.json 文件,执行后会提示你填写一些信息,如果你想让所有的内容保留默认,你可以加选项y,如`npm init -y` 执行此命令之后,**只会**在当前文件夹创建package.json这一个文件,此文件的内容大致如下 ```json { "name": "demo", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } ``` ### 包的安装 开发自己的项目时,如果要安装第三方的某个包就需要执行命令`npm install`或者其简写`npm i`来实现所依赖的包的安装,比如要安装cowsay这个包 ```powershell npm i cowsay ``` 上面的安装是本地安装,如果你想全局安装就加上参数-g,它会把包安装到nodejs的全局目录里,可以通过命令`npm root -g`查看包的全局目录是哪一个,一般是目录` \Users\用户名\AppData\Roaming\\npm\node_modules` > 个人建议:任何包的安装都不要采用全局安装的形式 全局安装之后,你就可以直接在命令行终端执行下面的命令来执行cowsay这个程序了 ```powershell cowsay haha ``` 这是因为安装nodejs时把全局放包的目录放置到了操作系统**当前用户**的环境变量里了。 如果你是本地安装,那么不能直接使用cowsay程序,要指定它所在的目录,比如下面的命令 ```powershell .\node_modules\.bin\cowsay haha ``` 像这种安装的包是可以直接执行的程序时,想运行它通常是在项目的`package.json`文件的`scripts`节进行配置,具体信息见npm run这一章节 ### 镜像源 安装npm包时默认使用的是国外的服务器,可以通过下面的命令查看当前源 ```powershell npm config get registry ``` 国外的服务器通常较慢,你通常会指定国内的某个服务器地址,这个地址一般是国外的官方服务器`https://registry.npmjs.org`的内容镜像,但可能不是实时同步的,国内一个比较好用的镜像地址是`https://registry.npmmirror.com`。 如果你想永久更换npm命令安装包时使用的服务器,那么就执行下面的命令 ```powershell npm config set registry https://registry.npmmirror.com ``` 需要换回为官方的镜像源时,执行下面命令即可 ```powershell npm config set registry https://registry.npmjs.org ``` 如果你只是想安装某个包时临时指定国内的镜像网址,可以使用下面的命令 ```powershell npm --registry https://registry.npmmirror.com ``` 比如安装cnpm包 ```powershell npm install -g cnpm --registry=https://registry.npmmirror.com ``` > cnpm功能与npm类似,只不过使用cnpm安装包时默认使用的是淘宝的镜像 > > 所以国内用户安装了cnpm之后通常会使用cnpm来安装包,而不再使用npm安装包 > > 但在实践中用cnpm安装也会带来很多问题,不建议用这种方式,还是建议用原本的npm来安装包,临时或永久的指定国内的某个镜像就可以了 ### 查看包信息 查看已安装包的信息,执行下面的命令,如果想看全局形式安装的包的信息就加上参数`-g ` ```shell npm ls npm ls -g ``` 上面的命令会把包的名字、版本以及依赖的所有包都显示出来,如果想控制层级就执行下面的命令 ```shell npm ls -g --depth 0 ``` 如果想看某个包的当前的详细信息,可以执行下面的命令 ```shell npm info cowsay ``` ### 包的版本 `npm`包的版本号遵从[语义化版本规范](https://link.juejin.cn?target=https%3A%2F%2Fsemver.org%2Flang%2Fzh-CN%2F),使用 `x.y.z` 形式,对应`MAJOR.MINOR.PATCH`,也即`主版本号.次版本号.修订版本号`,各位置的版本号的含义如下: | 版本号 | 含义 | 兼容性 | | ---------- | ------------------------------- | ------------ | | 主版本号 | 如有改变,表示对`API`作了改动 | **不兼容** | | 次版本号 | 如有改变,表示增加了新`feature` | **向下兼容** | | 修订版本号 | 如有改变,表示修复了存在的`bug` | **向下兼容** | > 详细情况见https://juejin.cn/post/7153655986702516260 安装`npm`包时,版本前符号的含义: - `*`:使用任意版本 - `^x.y.z`:`x` 位与指定版本一致,`y`、`z` 位使用最新版本 - `~x.y.z`:`x`和`y`位与指定版本一致,`z`位使用最新版本 - `x.y.z`和`@x.y.z`:`x`、`y` 和`z`全部锁死,使用指定的版本 你可以通过运行下面的命令来安装特定版本的软件包 ```bash npm install @ ``` 安装xxx最新版本的包可以执行下面的命令 ```powershell npm i xxx@latest ``` 如想查看包的最新版本信息,可以执行命令`npm view xxx versions` 或者 `npm info xxx` ### 开发依赖和生产依赖 通常你会看到更多的标志被添加到这个命令中: - `--save` 安装并添加条目到 `package.json` 文件的生产依赖(dependencies) - `--save-dev` 安装并添加条目到 `package.json` 文件开发依赖(devDependencies ) - `--no-save` 安装但不添加条目到 `package.json` 文件依赖 也可以使用标志的简写形式: - -S:`--save` - -D:`--save-dev` - -O:`--save-optional` devDependencies 和 dependencies 之间的区别在于前者包含开发工具,如测试库,而后者与生产中的应用打包在一起。 可以试试下面的命令加-D与不加的区别 ```powershell npm i -D qs ``` ### package-lock 安装包的时候,如果没有`package.json`文件,它会自动创建此文件,除了这个文件以外还会创建出`package-lock.json`文件以及用来存放本地安装包的目录`node_modules`文件夹出来 package.json文件只记录你通过npm install方式安装的模块信息,而这些模块所依赖的其他子模块的信息不会记录 package-lock.json文件锁定所有模块的版本号,包括主模块和所有依赖子模块。当你执行npm install的时候,node从package.json文件读取模块名称,从package-lock.json文件中获取版本号,然后进行下载或者更新 package.json中只是锁死了依赖项的版本,而没有锁死依赖项的依赖的版本,这里就是变数的地方。如果不对整个依赖树做锁定,那前后编译出来的应用版本可能是不一样的,有可能开发时能正常工作,而到了线上却不能工作。 ***当你从github,gitee等仓库下载一个项目后,通常此项目是没有依赖的,只有package.json与package-lock.json文件,这个时候你就直接运行`npm install`命令来安装这些依赖即可*** qs是一个处理查询字符串(Query String)的库,比如下面的代码 ```js const qs = require('qs') const obj = qs.parse('a=c') console.log(obj) const str = qs.stringify(obj) console.log(str) ``` 如果你直接执行命令`npm install qs` ,那么会自动创建package.json文件,里面记录的内容是(因为此时此刻最新版是6.11.2): ```json { "dependencies": { "qs": "^6.11.2" } } ``` 你在npmjs网站上搜索你会发现有`6.9.7`,`6.4.1`等版本,你可以执行下面的命令安装指定版本 ```shell npm install qs@6.4.1 ``` 此时package.json文件记录的内容如下 ```json { "dependencies": { "qs": "^6.4.1" } } ``` 如果你此时删除node_modules文件夹与package-lock.json文件,之后再执行`npm install`命令,它就安装6.11.2这个版本的qs库(可以在package-lock文件里看到安装的版本或者用命令npm list),因为package.json文件锁定的是大版本号6(符号^的含义就是大版本号) 如果你只是删除node_modules文件夹,然后再执行命令`npm install`,那么安装的仍然会是6.4.1这个版本,因为`package-lock.json`文件锁死的是6.4.1这个版本 ### 更新包 如果包在服务器已经有更新了,你想把本地已经安装的包更新到最新版本,可以执行下面的命令 ```bash npm update ``` `npm` 将检查所有包是否有满足你的版本控制约束的更新版本。 你也可以指定要更新的单个包: ```bash npm update ``` 假定你最初执行下面的命令安装了6.4.1这个版本的qs包 ```powershell npm install qs@6.4.1 ``` 执行之后,package-lock文件里的内容如下 ```json "packages": { "": { "dependencies": { "qs": "^6.4.1" } }, "node_modules/qs": { "version": "6.4.1", "resolved": "https://registry.npmmirror.com/qs/-/qs-6.4.1.tgz", "integrity": "sha512-LQy1Q1fcva/UsnP/6Iaa4lVeM49WiOitu2T4hZCyA/elLKu37L99qcBJk4VCCk+rdLvnMzfKyiN3SZTqdAZGSQ==", "engines": { "node": ">=0.6" } } } ``` 之后你执行npm update更新包就会更新到6这个版本的最新版本,也就是写此文档时的6.11.2这个版本 ```powershell npm update qs ``` 如果你想安装指定版本的包,在有package-lock文件的情况下,先更改package.json文件中的版本号,然后再执行`npm update qs`命令即可更新到指定版本的包(见下面的npm i与npm update的区别的说明) 如果要查看已经过期的包,执行命令`npm outdated` 即可了解 > > > **npm i 与npm update之间的区别** (https://juejin.cn/post/6913833065647341581) > > 下面描述: > > - `package` 表示 package.json 依赖版本管理文件 > - `lock` 表示 package-lock.json 锁定依赖版本文件 > > 1. `lock` 文件存在 > > - `npm i` 会按照 `lock` 对应包版本进行安装,不会自动升级 > - 手动更改 `package` 中对应包, `lock` 会按照 `package` 前缀版本规范更新到最新版本, `package` 版本为手动版本不变 > - `npm update` 会按照 `package` 对应包版本前缀升级规范安装到最新到版 > - `package` 中对应包版本号改变【按前缀规范最新版与 `lock` 中相同则不变,不同则改变】 > - `lock` 中对应包版本号改变 > > 1. `lock` 文件不存在 > > - `npm i` 会按照 `package.json` 对应包版本前缀升级规范安装到最新到版本 > - `package.json` 仍然是之前的前缀规范版本号 > - `package-lock.json` 中按版本前缀规范升级到最新的版本 > - `npm update` 和 `npm i` 相似 > - 但会忽略 `devDependencies` 下的对应包更新安装 > - 添加了 `-D` 才会在安装更新 `dependencies` 下的前提下更新安装 `devDependencies` 下对应的依赖包 > > ### 包的卸载 卸载包的命令如下 ```shell npm uninstall [<@scope>/][@]... [-S|--save|-D|--save-dev|-O|--save-optional|--no-save] aliases: remove, rm, r, un, unlink ``` 命令也可以简写为`npm un`,卸载时会处理package.json文件与node_modules文件夹下面的内容 `npm uninstall` 采用 3 个专有的可选标志,用于保存或更新主 package.json 中的包版本: - `-S, --save`:包将从您的 `dependencies` 中删除。 - `-D, --save-dev`:包将从您的 `devDependencies` 中删除。 - `--no-save`:包不会从您的 `package.json` 文件中删除。 比如下面的这些写法 ```shell npm uninstall sax --save npm un node-tap --save-dev npm un lodash --no-save ``` ### npm run 命令`npm run`会从包的package.json文件内的 `"scripts"` 对象运行你指定的脚本, 比如下面的package.json中的scripts配置,执行命令`npm run a`就是在执行`node ./src/main.js`,执行`npm run b`就是执行`dir`命令 ```json { "dependencies": { "qs": "^6.4.1" }, "scripts": { "a": "node ./src/main.js", "b": "dir", "c": "cowsay hello" } } ``` 执行`npm run`命令时如果没有指定脚本命令名那么就会列出所有的脚本信息 ```shell > npm run Scripts available in undefined via `npm run-script`: a node ./src/main.js b dir c cowsay hello ``` 执行`npm run a`等价于执行命令`node ./src/main.js`. 使用`npm run` 执行脚本的时候会创建一个shell,然后在shell中执行指定的脚本,这个shell会将当前项目的可执行目录(即`node_modules/.bin`)添加到环境变量path中,当脚本执行之后再恢复原样,也就是说脚本命令中的程序名会直接到node_modules/.bin中去找,不需要加上路径,比如下面的脚本配置 ```js //假定安装了cowsay包的话 npm i cowsay "c":"cowsay haha" ``` 否则你就需要像下面的方式来执行cowsay程序 ```powershell .\node_modules\.bin\cowsay haha ``` 在脚本中可以指定多个任务,如果想让其并行执行就用`&`符号,比如 ```js "d":"node test.js & cowsay haha" ``` 如果想让其依次执行,也就是前面执行完了才执行后面的话,就用`&&`符号 ```js "e":"node test.js && cowsay haha" ``` > `env`脚本是一个特殊的的内置命令,可用于列出脚本在运行时可用的环境变量。如果您的包中定义了 "env" 命令,它将优先于内置命令 > > ```shell > > npm run env > > > env > > SET > > ALLUSERSPROFILE=C:\ProgramData > APPDATA=C:\Users\Administrator\AppData\Roaming > CHROME_CRASHPAD_PIPE_NAME=\\.\pipe\crashpad_1012_UQYRZLVVFODRADEM > CLASSPATH=.;C:\Program Files\Java\jdk1.8.0_291\lib;C:\Program Files\Java\jdk1.8.0_291\lib\tools.jar; > COLOR=1 > COLORTERM=truecolor > CommonProgramFiles=C:\Program Files\Common Files > CommonProgramFiles(x86)=C:\Program Files (x86)\Common Files > CommonProgramW6432=C:\Program Files\Common Files > Path=D:\tmp\test\node_modules\.bin;D:\tmp\node_modules\.bin;D:\node_modules\.bin;C:\Program Files\nodejs\node_modules\npm\node_modules\@npmcli\run-script\lib\node-gyp-bin;C:\Program Files (x86)\VMware\VMware Workstation\bin\;C:\Program Files\Java\jdk1.8.0_291\bin;C:\Program Files\Java\jdk1.8.0_291\jre\bin; > .......(省略) > ``` > > ### 常见的库 https://juejin.cn/post/7002054481252728869 https://juejin.cn/post/6890702514446925838 # 环境变量 环境变量 (environment variables) 是在操作系统中用来指定操作系统运行环境的一些参数 ## windows命令行与powershell 在windows的命令行(Windows Command简称CMD)中通过set来创建或者设置环境变量,通过echo来读取,比如 ```shell set aaa=123 echo %aaa% ``` PowerShell 把所有环境变量的记录保存在 env: 虚拟驱动中,`ls env:` 命令可以列出所有环境变量。根据环境变量的名称就可以使用 $env:name 访问。 ```powershell ls env: ``` 可以参考命令行中的读取变量名写法,将命令行中的 %变量名% 替换为 PowerShell 的格式的变量名写法 比如 CMD 中的 %windir% 替换成 PowerShell 就是 $env:windir: ```powershell PS> $env:windir C:\Windows ``` 创建或删除环境变量写法如下 ```powershell $env:aaa="222" $env:aaa del env:aaa $env:aaa ``` 追加环境变量值的写法是(用分号分隔):$env:变量名称="$env:变量名称;变量值" 比如,常见的在 PATH 后面追加一条 ```powershell $env:Path="$env:Path;C:\sysin" ``` > https://zhuanlan.zhihu.com/p/349455443 (windows系统环境变量,命令行与powershell两种情况下的设置) ## nodejs中环境变量 > https://www.sitepoint.com/node-js-environment-variables-how-to-set-them-properly/ 访问nodejs中的环境变量主要是靠`process.env`这个对象,此全局对象以键值对的形式持有了所有的环境变量。 假定你有下面的一个js文件,用`node test.js`执行时就可以输出此环境变量的内容 ```js //创建了一个环境变量abc,值为cj process.env.abc = 'cj' // 输出环境变量abc的值 console.log(process.env.abc) // 输出所有的环境变量 console.log(process.env) console.log(process.env.def) ``` 上面的def是没有这个环境变量的,直接执行此js文件时输出的值是undefined,你可以通过在命令行或者powershell中创建一个环境变量def(linux或mac可以用export命令实现),这样输出就有值了 ```powershell $env:def=123 node .\src\test.js cj 123 ``` ## dotenv 当我们的项目需要声明很多环境变量的时候,命令行声明的形式显然过于繁琐,而且难以管理,这个时候可以借助dotenv库来处理环境变量,其支持把环境变量放置在一个文件中,并把文件的内容读取之后放置到process.env里,其最基本的使用分为以下步骤 - 安装:`npm i -D dotenv` - 项目根目录创建存放环境变量的文件,比如叫`.env`,内容如下 ```env aaa=123 bbb=456 ``` - 在文件中引入dotenv库并读取文件,代码如下 ```js //test.js const dotenv = require('dotenv') dotenv.config() console.log(process.env.aaa) console.log(process.env.bbb) ``` 执行命令`node ./src/test.js`输出结果如下 ```powershell 123 456 ``` 如果你的环境变量文件不是默认的文件名或者不在项目根目录下,比如放在项目根目录下的`config/env/.env.dev`,那么就需要指定路径与名字了 ```js //test.js const path = require('path') const dotenv = require('dotenv') // 只配path就够了,encoding与debug是可以不配置的 const envConfig = dotenv.config({ path: path.resolve(__dirname, 'config/env/.env.dev'), // 配置文件路径 encoding: 'utf8', // 编码方式,默认utf8 debug: false, // 是否开启debug,默认false }).parsed //下面这个if块是可以删除掉的 if (!envConfig) { console.log('环境配置文件不存在') // 退出程序 process.exit(1) } console.log(process.env.aaa) console.log(process.env.bbb) ``` 如果你有多个环境变量文件,分别存放不同运行环境下的环境变量,比如.env.dev存放开发环境下的环境变量,.env.prod存放生产环境下的环境变量,那么就只需要一点编码技巧就可以实现,比如下面的代码 ```js const path = require('path') const dotenv = require('dotenv') const envConfigPath = { dev: path.resolve(__dirname, 'config/env/.env.dev'), // 开发环境配置 prod: path.resolve(__dirname, 'config/env/.env.prod'), // 正式环境配置 } // CURRENT_ENV是一个环境变量,可以在终端中指定 const envConfig = dotenv.config({ path: envConfigPath[process.env.CURRENT_ENV], }).parsed console.log(process.env.aaa) console.log(process.env.bbb) ``` 执行命令`node .\src\test.js`之前,先在终端设置环境变量的值,比如在powershell下就执行下面的命令 ```powershell $env:CURRENT_ENV="prod" ``` 那么最终会输出.env.prod文件下的aaa与bbb的值,结果是 ```shell 777 888 ``` 如果你多个环境文件有一些共同的配置,不想在每个环境文件中重复编写,那你就创建出一个存放共同配置的环境文件出来,比如`.env.common`,假定下面的内容 ```env aaa=1 bbb=4 ``` 很明显,环境特定的文件与通用文件中有些变量的key值可能是一样的,那以谁的为准呢?dotenv的准则是谁先读取就以谁为准,比如下面的代码 ```js const path = require('path') const dotenv = require('dotenv') const envConfigPath = { common: path.resolve(__dirname,'config/env/.env.common'), dev: path.resolve(__dirname, 'config/env/.env.dev'), prod: path.resolve(__dirname, 'config/env/.env.prod'), } const commonConfig = dotenv.config({ path: envConfigPath.common, }).parsed // CURRENT_ENV是一个环境变量,可以在终端中指定 const envConfig = dotenv.config({ path: envConfigPath[process.env.CURRENT_ENV], }).parsed // commonConfig在前面,所以aaa,bbb输出的是common文件的值 console.log(process.env.aaa) console.log(process.env.bbb) ``` 如果你调整envConfig与commonConfig的位置,输出的值就会不一样了 > https://juejin.cn/post/6993224664705138702 # Webpack 本质上,**webpack** 是一个用于现代 JavaScript 应用程序的 *静态模块打包工具*。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 [依赖图(dependency graph)](https://www.webpackjs.com/concepts/dependency-graph/),然后将你项目中所需的每一个模块组合成一个或多个 *bundles*,它们均为静态资源,用于展示你的内容。 从 v4.0.0 开始,**webpack 可以不用再引入一个配置文件**来打包项目,然而,它仍然有着 [高度可配置性](https://www.webpackjs.com/configuration),可以很好满足你的需求。 ## 安装 webpack 从 4.0 版本开始,在安装时,就必须要安装webpack 与webpack-cli这两个东西。 webpack 是打包代码时依赖的核心内容,而 webpack-cli 是一个用来在命令行中运行 webpack 的工具。它主要用来收集命令行参数并利用这些参数来构建Compiler对象。 webpack-cli不是必须的,你完全可以构建自己的cli,在https://juejin.cn/post/7091597732107943966此文中有详细分析 安装 webapck 时把 webpack-cli 也装上是因为在 webpack4.x 版本后 webpack 模块把一些功能分到了 webpack-cli 模块,所以两者都需要安装 ```shell npm install -D webpack webpack-cli ``` 如果你是全局安装的方式,那么就可以直接使用命令webpack来做一些事情,如果你是本地安装webpack,那么就可以像下面这样使用webpack命令来执行打包操作了,其中src、dist、node_modules三个目录是平级的,都是项目的直接子目录 ```powershell .\node_modules\.bin\webpack ./src/test.js -o ./dist/abc.js ``` 但通常不会直接使用webpack命令,通常是在项目初始化的package.json文件的脚本中间接使用命令webpack,比如下面 ```json "scripts": { "build": "webpack" } ``` 直接执行`npm run build`会告诉你一些错误信息,主要是在src目录下找不到index.js文件。 如果你在正确的目录中创建了对应的index.js文件,再次运行npm run build就不会报错了,只是有一个mode未设置的警告信息。 > 可以在src目录下建立一个index.js文件,内容如下 > > ```js > function add(first, second) { > return first + second > } > const r = add(5, 6) > console.log(r) > ``` > > 执行`npm run build`之后看看生成的main.js文件内容 > > 之后把index.js文件内容改成下面这样 > > ```js > function add(first, second) { > return first + second > } > const r = add(5, 6) > console.log(r) > > const r2 = add(50, 6) > console.log(r2) > ``` > > 再次执行`npm run build`之后看看生成的main.js文件内容,比较两次main.js文件内容的不同来体会webpack这个构建工具的基本作用 ## 配置 配置信息可以利用命令行参数的形式提供,也可以在配置文件中编写来提供给webpack命令,比如上文提到的mode警告信息,你就可以在package.json文件的脚本中改成下面的配置来提供 ```js "build": "webpack --mode=development" ``` 如果想创建配置文件,你就新建一个配置文件(通常是一个js文件,但webpack也支持其它语言编写的配置文件,假定叫webpack.config.js,内容如下 ```js //此文件在config目录下,config目录与src目录平级 module.exports = { mode: 'production', }; ``` 接着你在package.json文件中改为下面的形式 ```js "build": "webpack --config=./config/webpack.config.js" ``` ### entry **入口起点(entry point)** 指示 webpack 应该使用哪个模块,来作为构建其内部 [依赖图(dependency graph)](https://www.webpackjs.com/concepts/dependency-graph/) 的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。 默认值是 `./src/index.js`,但你可以通过在 [webpack configuration](https://www.webpackjs.com/configuration) 中配置 `entry` 属性,来指定一个(或多个)不同的入口起点。 ```javascript module.exports = { entry: './path/to/my/entry/file.js', }; ``` 上面配置是下面的简写形式 ```javascript module.exports = { entry: { main: './path/to/my/entry/file.js', }, }; ``` 入口配置的详细情况见https://www.webpackjs.com/concepts/entry-points/ ### 文件名配置 可以通过配置 `output` 选项,告知 webpack 如何向硬盘写入编译文件。注意,即使可以存在多个 `entry` 起点,但只能指定一个 `output` 配置。 在 webpack 配置中,`output` 属性的最低要求是,将它的值设置为一个对象,然后为将输出文件的文件名配置为一个 [`output.filename`](https://www.webpackjs.com/configuration/output/#outputfilename): ```javascript module.exports = { output: { filename: 'bundle.js', }, }; ``` 此配置将一个单独的 `bundle.js` 文件输出到 `dist` 目录中。详细情况见https://www.webpackjs.com/concepts/output/ 下面是常见的一个输入输出配置 ```js module.exports = { mode: 'production', entry: './src/index.js', output: { filename: 'bundle.js', }, } ``` ### 清理输出 你可以在output节配置clean:true用来在每次构建前清理dist目录内容 ```js output: { filename: 'bundle.js', clean: true }, ``` ### 文件路径 如果你要设定最终build出来的文件不放在默认的dist目录,你可以利用path属性进行配置,这通常会需要引入path模块到webpack配置文件中 ```js const path = require('path') module.exports = { mode: 'development', entry: './src/index.js', output: { path: path.resolve(__dirname, '../dist'), filename: 'bundle.js', } } ``` ### hash处理 使用 webpack 进行打包,每个资源都可以生成一个带 hash 的名字。浏览器可以利用该 hash 能够做到资源缓存,访问性能提升,详细情况见https://juejin.cn/post/7146489193491857421,比如下面的output配置 ```js output: { path: path.resolve(__dirname, '../dist'), filename: 'bundle.[contenthash].js', } ``` > Q:为什么文件需要hash? > > A:如果不使用文件hash,那么就不能设置缓存(如果你设置缓存,因为你的代码可能会变更,但是浏览器会命中缓存,从而获取不到最新的代码),每次都必须从服务器中获取,造成资源浪费不说,用户打开网站的速度也可能缓慢。但是我们使用hash,在代码(hash)没有发生变更之前,只要设置的缓存时间没有到期,都可以利用缓存来帮助我们做优化。 ## loader loader 用于对模块的源代码进行转换。loader 可以使你在 `import` 或 "load(加载)" 模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式。 loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL,loader 甚至允许你直接在 JavaScript 模块中 `import` CSS 文件! > 阅读https://juejin.cn/post/6992754161221632030深入了解loader的原理 ### ts-loader 此loader是用来处理typescript脚本文件的,比如你有下面的ts文件 ```js //./src/myts.ts const age: number = 24 console.log(age) const say = () => { console.log('hello world') } say() const arr = [1, 2, 3] console.log(arr.includes(4)) ``` 然后你直接修改webpack.config.js文件中的entry为`entry: './src/myts.ts'`,那么你运行`npm run build`命令会报没有对应的loader的错误 你执行下面的命令安装ts-loader ```shell npm i -D ts-loader ``` 并在webpack.config.js文件中配置如下的内容 ```js module: { rules: [ { test: /\.ts$/, use: ['ts-loader'], }, ], }, ``` 再次运行npm run build命令会报如下的错误 ```shell ERROR TS18002: The 'files' list in config file 'tsconfig.json' is empty. ts-loader-default_e3b0c44298fc1c14 ``` tsconfig.json是ts编译处理的默认配置文件,依据这个错误信息,你需要在根目录下建立tsconfig.json文件,建立之后(里面可以没有内容),再次build就不报错了,但通常tsconfig.json文件可以有如下的配置(大概意思就是把typescript代码转换成es5规格的javascript代码) ```json { "compilerOptions": { "target": "es5" }, "exclude": ["./node_modules"] } ``` ### css-loader css-loader 用来识别并加载 css,编写如下的css文件 ```css h1 { color: red; } ``` 在入口文件index.js中导入css文件 ```js import mycss from './mycss.css' ``` 先执行下面的命令安装css-loader ```shell npm i -D css-loader ``` 接着在rules中配置规则 ```js { test: /\.css$/, // css提取插件不再需要style-loader,注意不要加引号 use: 'css-loader', } ``` 之后执行npm run build命令进行构建,就会在最终的结果文件bundle中看到样式相关的代码在里面了 ### style-loader style-loader是用来在html页面中插入一个style元素,也就是插入样式文件用的,把配置文件改为如下形式 ```js rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'], }, ] ``` 那么构建后的js文件中就会简单插入style相关的代码(搜索createElement("style")就可以搜索到) 如果有多个loader,那么处理顺序是从右往左执行的 > 如果你想看到这些样式放置在html的效果,可以先安装html-webpack-plugin插件,接着在webpack的配置文件中进行如下的配置 > > ```js > const HtmlPlugin = require('html-webpack-plugin') > module.exports = { > plugins: [ > new HtmlPlugin({ > template: './src/index.html', > }), > ], > } > ``` > > 接着你就在src目录下建立对应的index.html文件与上面配置的template值对应起来,html文件的核心内容如下 > > ```html > >

h111111

> > ``` > > 之后直接运行npm run build,那么webpack就会把entry配置的js文件处理之后的结果文件main.js插入到这个template设定的html文件中,并在dist目录生成最终的html文件 ### postcss-loader 通常你编写的css是比较现代的写法,但某些浏览器并不支持,需要经过一些后续的处理,比如下面的样式是让浏览器的label标签不能被选中 ```css label { user-select: none; } ``` 但各个不同的浏览器要达成这个不能被选中的效果就需要添加各个浏览器自己的前缀,比如下面的前缀就是chrome浏览器支持的 ```css label { -webkit-user-select: none; -moz-user-select: none; user-select: none; } ``` 类似这样的后置处理,主要是靠postcss这个loader来实现的,想使用它就需要先安装 ```powershell npm install postcss-loader postcss -D --registry=https://registry.npmmirror.com ``` postcss自己本身也支持插件,像上面的添加前缀的功能是靠autoprefixer插件实现的,现在基本不使用autoprefixer插件,而是使用postcss-preset-env,它可以帮助我们将一些现代的 `CSS` 特性,转成大多数浏览器认识的 `CSS`,并且会根据目标浏览器或运行时环境添加所需的 `polyfill`,安装方法如下 ```powershell npm i postcss-preset-env -D --registry=https://registry.npmmirror.com ``` 安装完毕之后你就可以使用这个loader了,方法如下 ```js //在webpack配置文件的rules下面 { test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'], } ``` 如果想转换sass文件,就像下面这样配置 ```js { test: /\.s[ac]ss$/i, use: [ // 将 JS 字符串生成为 style 节点 'style-loader', // 将 CSS 转化成 CommonJS 模块 'css-loader', 'postcss-loader', // 将 Sass 编译成 CSS 'sass-loader', ], } ``` > sass的loader的使用见后面章节SPA中的样式处理小节 postcss这个loader本身自己的一些配置,可以在webpack使用此loader的地方配置也可以创建一个单独的文件`postcss.config.js`进行专门的配置 ```js module.exports = { plugins: [ [ 'postcss-preset-env', { // 其他选项 }, ], ], } ``` ### url-loader与file-loader 我们希望在页面引入图片(包括 img 的 src 和 background 的 url)或设置字体,但是当我们基于 webpack 进行开发时,引入图片会出现诸如路径,图片是否需要压缩,转换等问题,这两个loader可以帮助解决问题 file-loader 可以解析项目中的 url 引入(不仅限于 css),根据我们的配置,将图片拷贝到相应的路径,修改打包后文件引用路径,使之指向正确的文件。 url-loader 允许你有条件地将文件转换为内联的 base-64 URL (当文件小于给定的阈值),这会减少小文件的 HTTP 请求数。如果文件大于该阈值,会自动的交给 file-loader 处理。 简答地说,url-loader 封装了 file-loader。url-loader 不依赖于 file-loader,即使用 url-loader 时,只需要安装 url-loader 即可,不需要安装 file-loader,因为 url-loader 内置了 file-loader。 在webpack 5版本已经不推荐使用这些loader了,改为使用自带的asset module功能,详见https://webpack.js.org/guides/asset-modules/ > 可以使用npm ls命令了解当前项目安装的所有模块以及版本 Asset Modules 提供了4种资源模块的类型,而无需再额外配置其他的 loader 来对资源模块进行处理,分别是: - `asset/resource` :构建出一个单独的文件并导出 URL。相当于以前的 `file-loader`。 - `asset/inline` :导出资源模块的Data URI 内嵌到 html 或 css 文件。相当于以前的 `url-loader`。 - `asset/source` :导出资源模块的源代码。相当于以前的 `raw-loader`。 - `asset` :在导出 Data URI 和导出一个单独文件之间自动进行选择。以前可以通过使用 `url-loader` 的 `limit` 配置项来实现。 下面是一个典型的配置,其含义是当图片文件大于12k(默认是8k)时生成单独的文件,否则就变成base64编码,文件的路径与名字通过generator的filename指定(ext指文件原来的扩展名) ```js { test: /.(png|jpe?g|gif|webp|svg)$/, type: 'asset', generator: { filename: 'img/[name].[hash:8][ext]', }, parser: { // 生成Data URI 的条件 dataUrlCondition: { // 默认不超过 4kb 时,生成 DataURI, //超过 4kb 时,单独打包成文件 maxSize: 12 * 1024, // 12kb }, }, }, { test: /.(eot|ttf|otf|woff2?)$/, type: 'asset', generator: { filename: 'fonts/[name].[hash:8][ext]', }, } ``` > 详细的使用说明可以参考https://juejin.cn/post/7133728581590450190 你可以在src目录下建立一个assets/images的两级目录,并在此images目录下放置两张图片,一个大于12kb,一个小于12kb,并在src目录下建立index.js与一个css文件,一个html模版文件,之后就可以测试效果了(启动开发服务器或者运行命令npm run build)看看是否在dist目录生成了符合设定的文件名以及路径 html文件参考内容如下 ```html
``` js文件参考内容如下 ```js import mycss from './mycss.css' ``` css文件如下 ```css .assettest1 { width: 100px; height: 100px; background-size: cover; background-image: url(./assets/images/1.jpg); } .assettest2 { width: 100px; height: 100px; background-size: cover; background-image: url(./assets/images/2.jpg); } ``` ### babel-loader `Babel` 是一个 `JavaScript` 编译器。`Babel` 是一个工具链,主要用于将采用 `ECMAScript 2015+` 语法编写的代码转换为向后兼容的 `JavaScript` 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。 下面列出的是 `Babel` 能为你做的事情: - 语法转换:高级语言特性的降级 - polyfill:通过 Polyfill 方式在目标环境中添加缺失的特性 - 源码转换:我们可以将 jsx、vue 代码转换为浏览器可识别的 JS 代码。 > https://juejin.cn/post/7126465727178997791 (建议先看这篇文章) 我们通常会使用预设(preset)来处理转换,预设就是一组插件,相当于你只要安装了我这么一个预设,就能享受到我这个预设里面所有的插件。 官方 `Preset` 有如下几个 - @babel/preset-env,将高版本js编译成低版本js - @babel/preset-flow,对使用了flow的js代码编译成js文件 - @babel/preset-react,编译react的jsx文件 - @babel/preset-typescript,将ts文件编译成js文件 下面我们使用`@babel/preset-env`这个预设来进行编译。 `@babel/preset-env` 主要作用是对我们所使用的并且目标浏览器中缺失的功能进行代码转换和加载 `polyfill`,在不进行任何配置的情况下,`@babel/preset-env` 所包含的插件将支持所有最新的JS特性(ES2015,ES2016等,不包含 stage 阶段),将其转换成`ES5`代码。 首先安装如下的库 ```shell npm install -D babel-loader @babel/core @babel/preset-env ``` 接着创建一个babel的配置文件,其支持的格式有很多,下面使用的是json格式配置文件,一般在根目录下,名字为babel.config.json ```json { "presets": ["@babel/preset-env"], } ``` 接着在webpack配置文件中使用label-loader 配置 ```js { test: /\.js$/, exclude: /(node_modules|bower_components)/, //排除掉node_module目录 use: { loader: 'babel-loader' }, } ``` 要转换的js ```js let users = ['aaa', 'bbb'] users.forEach((item) => console.log(item)) ``` 转换之后的代码是把箭头函数换成了传统的方式,下面是转换之后的js的部分代码 ```js var __webpack_modules__={"./src/main.js":()=>{eval("var users = ['aaa', 'bbb'];\nusers.forEach(function (item) {\n return console.log(item);\n}) ``` 如果你在babel的配置文件中写成这样,那么代码基本没有任何转换,因为较新的浏览器是支持箭头函数的 ```json { "presets": ["@babel/preset-env"], "targets": "last 2 Chrome versions" } ``` ## plugin **插件** 是 webpack 的 [支柱](https://github.com/webpack/tapable) 功能。插件目的在于解决 [loader](https://www.webpackjs.com/concepts/loaders) 无法实现的**其他事**,插件可以执行范围更广的任务,包括打包优化,资源管理,注入环境变量,其实插件就是暴露了 webpack 整个打包构建生命周期 中的钩子给我们订阅,方便我们监听整个打包过程(区别见https://juejin.cn/post/7098556679242907662) ### 插件使用 插件的使用基本包含以下几个步骤 - 安装插件 `npm i xxx` - 声明插件 `const xxx = require(xxx-webpack-plugin)` - 插件使用配置(可选的),一般都是在webpack配置文件的plugins元素里 - 使用插件`xxx`,这个`xxx`就是第二步声明的名字 ```js plugins:[ new XxxPlugin(), new YyyPlugin({ option1: '配置值1', option2: '配置值2', ... }), ] ``` ### path 插件 path插件是一个自带的插件,是用来处理路径相关功能的,不需要再额外安装,比如下面的用法 ```js const path = require('path') module.exports = { mode: 'production', entry: './src/index.js', output: { path: path.resolve(__dirname, '../dist'), filename: 'bundle.js', clean: true, } } ``` 其中__dirname是一个变量名,表示当前目录,也就是此配置文件webpack.config.js所在的目录config(此目录在项目根目录下,不在根目录下的src目录里),上面的配置意思就是把最终生成的bundle文件放置在与config目录平级的dist目录里 ### HtmlWebpackPlugin HtmlWebpackPlugin可以自动帮我们将 webpack 打包生成的文件(比如 js 文件、css 文件)嵌入到 html 文件中。这在生成的文件带有哈希串时尤为有用。 使用此插件的基本步骤是 - 安装:执行命令`npm i -D html-webpack-plugin` - 导入插件:在webpack配置文件中导入插件模块 - 应用插件并配置 比如下面是一个典型的配置 ```js const HtmlWebpackPlugin = require('html-webpack-plugin'); const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', clean: true }, plugins: [new HtmlWebpackPlugin()], }; ``` 配置好之后,直接执行build脚本就会自动创建一个index.html文件,此文件内部会把构建出的bundle脚本嵌入到此html文件中 如果你要对此插件进行一些额外的设置,可以在实例化时传递一个对象型参数,其常用属性及含义如下 - title:设置网页标题 - filename:生成 html 文件名,默认值为 `index/html` - template:使用自己的模板,这里填这个模板的路径,使用了之后一些配置项就无效了,比如 title - favicon:指定网站图标路径,除了会在 html 上填充 favicon 相关内容,**还会将该文件拷贝到打包文件夹下**,非常好用 - minify:是否压缩 html 文件。不设置时,如果 webpack 的 mode 为 `production`,就会压缩 html,移除多余的空格和注释之类的。 下面的代码是一个典型的配置 ```js new HtmlPlugin({ template: './src/myhtml.html', title: 'html plugin test', xxx: '自定义变量的值', }) ``` 下面是对应的模版html文件,这里面用到了插件的选项变量,详细用法见此插件的官方github网址https://github.com/jantimon/html-webpack-plugin#options ```html <%= htmlWebpackPlugin.options.title %>

<%= htmlWebpackPlugin.options.xxx %>

``` 注意,如果配置的entry js文件导入了css,并且也设置了相关的loader,那么生成的html中是会看到样式相关内容,最终的html内容如下 ![image-20230706090959585](images/htmlplugin1.png) ### CopyWebpackPlugin 此插件是用来拷贝单独的某个文件或者整个目录到构建目录里去,安装方法如下 ```powershell npm install copy-webpack-plugin --D ``` 安装完毕之后,在webpack的配置文件中配置即可使用,详细说明见https://www.webpackjs.com/plugins/copy-webpack-plugin/ ```js const CopyPlugin = require('copy-webpack-plugin') plugins: [ new CopyPlugin({ patterns: [ { from: './public/js', to: './js' }, { from: './public/images', to: './images' }, ], }) ] ``` ### MiniCssExtractPlugin 本插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。 本插件基于 webpack v5 的新特性构建,并且需要 webpack 5 才能正常工作。 与 extract-text-webpack-plugin 相比: - 异步加载 - 没有重复的编译(性能) - 更容易使用 - 特别针对 CSS 开发 > https://juejin.cn/post/6850418117500715015 > > https://webpack.docschina.org/plugins/mini-css-extract-plugin/ > > https://juejin.cn/post/7026742035667222559 基本的使用方法是 - 安装:`npm i -D mini-css-extract-plugin` - 配置:包含引入模块,应用插件及可选的提供参数设置,css规则中添加相关loader 典型用法如下 ```js //1.引入模块 const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { module: { rules: [ { test: /\.css$/, //3.设置loader,在html中嵌入link以引用生成的css文件, // 不需要style-loader use: [MiniCssExtractPlugin.loader, 'css-loader'], }, ], }, plugins: [ new HtmlPlugin(), //2.应用插件 new MiniCssExtractPlugin({ //此选项决定了输出的每个 CSS 文件的名称 filename: 'css/[name].css', }), ], } ``` 有一点要注意,你在css中用// 写注释时,此插件执行时可能报`Unexpected '/'. Escaping special characters with \ may hel` 这样的错误,尽量用/* */这种形式写css注释 使用了MiniCssExtractPlugin.loader就不需要使用`style-loader`了,因为它是以link的形式插入css文件的 ```html ``` > 使用的loader不像使用css-loader时要加单引号,它是不需要加引号的 ### CssMinimizerWebpackPlugin 这个插件使用 [cssnano](https://cssnano.co/) 优化和压缩 CSS,在webpack5下使用CssMinimizerWebpackPlugin插件来优化css,详细情况见https://webpack.docschina.org/plugins/css-minimizer-webpack-plugin/ 基本配置情况如下 ```js const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); module.exports = { module: { }, optimization: { minimizer: [ new CssMinimizerPlugin(), ], //默认只在生成环境优化,设置为true开发环境也优化 minimize: true, }, plugins: [], }; ``` ### terser-webpack-plugin webpack5已经内置了 `terser-webpack-plugin` 用于代码压缩,取代了之前版本的 `uglifyjs-webpack-plugin` 典型配置如下 ```js const TerserPlugin = require('terser-webpack-plugin'); module.exports = { optimization: { minimize: true, minimizer: [ new TerserPlugin({ // 在这里进行更进一步的配置 }), ], }, }; ``` 你可以编写下面的代码进行测试看看压缩效果 ```js function add(a, b) { return a + b } let r = add(5, 6) let r2 = add(50, 6) console.log(r) console.log(r2) ``` ### Compression-webpack-plugin “compression-webpack-plugin”插件能够通过压缩算法,将前端打包好的资源文件进一步压缩,生成指定的、体积更小的压缩文件,让浏览器能够更快的加载资源。官方文档见https://webpack.docschina.org/plugins/compression-webpack-plugin/ 安装压缩插件 ```powershell npm i -D compression-webpack-plugin --registry=https://registry.npmmirror.com ``` 配置如下: ```js const CompressionPlugin = require('compression-webpack-plugin') plugins: [ new CompressionPlugin(), ], ``` 打包后生成的压缩文件,不能直接使用,需要服务端配置才可以使用,而且发现打包生成的“dist/index.html”首页内,也没有直接引用这些“.gz”格式的文件。 而实现的关键,其实就是让服务端向浏览器发送“Content-Encoding=gzip”这个响应头,并把对应的“.gz”格式文件发送给浏览器,让浏览器通过“gzip”编码格式来解析资源。见https://segmentfault.com/a/1190000040268844这篇文章的说明,比如nginx的配置如下 ```nginx http { gzip_static on; } ``` ### DefinePlugin 相信很多前端开发者都会遇到过类似的场景:在开发阶段打印一些日志用于调试,在生产环境需要把这些日志去掉,并且不能让它们打包进去。比如以下代码在开发阶段会打印日志,在生产环境下则不会: ```js if (__DEV__) { console.log('当前版本:' + __VERSION__); } ``` 这种全局定义常量的功能就可以通过webpack自带的插件DefinePlugin实现,使用方法是:现在webpack的配置文件的plugins节先定义常量,比如下面 ```js plugins: [ new DefinePlugin({ __DEV__: true, __VERSION__: JSON.stringify('1.0.0'), }), ] ``` > 至于其中字符串值为什么用JSON.stringify,见官网https://webpack.js.org/plugins/define-plugin/ 接着你就可以在你的代码文件中使用这些常量了,比如index.js中编写上面的if代码块。当你把__DEV__常量的值改为false时,打包时输出版本的那行代码就不会出现在打包后的结果js文件里,好像这段代码压根没写过一样 如果全局常量的值不是写死在配置文件中,而是读取的环境变量的值,比如下面这种写法 ```js plugins: [ new DefinePlugin({ __DEV__: process.env.devMode, __VERSION__: JSON.stringify('1.0.0'), }), ] ``` 那么__DEV__这个变量的值就会随着环境变量的变化而变化,比如你在执行npm run命令前在powershell中输入下面的命令来设置环境变量的值就会影响最终打包文件的代码了 ```powershell # 值改成true试试 $env:devMode="false" ``` 需要注意的是,通过DefinePlugin插件定义的常量在业务脚本中是可以使用的,但在webpack的配置文件中就不能使用了,比如在webpack.config.js文件中编写下面的代码都是会报错的 ```js console.log(process.env.__VERSION__)// 输出undefined console.log(__VERSION__) //这行报错,说没有定义 ``` ## Webpack-dev-server 每次修改代码之后都要重新build一下才能看到最新代码效果是一个比较没有效率的工作,而webpack-dev-server就帮我们解决了这个问题 多数场景中,使用 webpack-dev-server 即可满足需要,它提供了一个简单的 web 服务器,并且能够实时重新加载(live reloading),基本使用方法见https://juejin.cn/post/7152066964083441671 - 安装:`npm i -D webpack-dev-server` - 配置 ```js devServer: { static: { directory: path.join(__dirname, './dist'), }, open: true, // 直接打开 port: 3000, hot: true } ``` - 添加运行脚本 ```json "scripts": { "build": "webpack --config ./config/webpack.config.js", "dev":"webpack-dev-server --config ./config/webpack.config.js" } ``` 现在直接执行脚本`npm run dev`启动开发服务器就会直接打开浏览器并显示默认页面,现在就可以直接更改源码文件,比如css文件,直接就可以在浏览器中看效果而不需要再重新执行脚本`npm run build`了 > 注意:开发服务器启动之后,修改entry设置指定的文件以及被此文件引用的文件时会立即看到效果,但修改webpack的配置文件以及package.json这样的文件是不会看到修改后的效果的,仍然需要重启开发服务器 webpack-dev-server 会将打包后的产物放入内存中,这样就大大提升了 构建速度与访问速度,同时也不会有磁盘的 IO 开销,延长硬盘使用寿命。要想看到实际打包的结果文件,需要自行进行打包服务 ## 多配置文件 有时候在一个项目里,我们的环境并不是只有一个,可能会分为多个环境,比如:开发环境、生产环境。这个时候如果把开发环境的配置和生产环境的配置都冗余在一起就很不合理。分开成多个配置文件单独编写的话又可能有重复的配置内容,解决这些问题就需要多配置文件与文件合并相关的功能了。 这个时候,我们可以使用 webpack-merge 插件,执行下面的命令安装此工具 ```shell npm i -D webpack-merge ``` 接着把原来的webpack.config.js改名为webpack.common.js,并新增两个文件webpack.dev.js,webpack.prod.js,分别代表开发环境与生产环境的配置文件, webpack.common.js代表两者的一些共同的配置,接着你可以按照职责把相关的配置分开,这里为了简单起见,主要把devServer的相关设置从common里删掉并添加到dev里了,dev文件的配置如下 ```js const { merge } = require('webpack-merge') const common = require('./webpack.common.js') const path = require('path') module.exports = merge(common, { mode: 'development', devServer: { static: { directory: path.join(__dirname, './dist'), }, open: true, // 直接打开 port: 3000, }, }) ``` prod文件的配置内容如下 ```js const { merge } = require('webpack-merge') const common = require('./webpack.common.js') module.exports = merge(common, { mode: 'production', }) ``` 接着在package.json文件的scripts里使用上这两个文件即可 ```json "scripts": { "dev": "webpack-dev-server --config ./config/webpack.dev.js", "prod": "webpack-dev-server --config ./config/webpack.prod.js" } ``` ## mode与环境变量 webpack 的 mode 提供三个值:none、development(开发模式)、production(生产模式,默认)。如果没有定义 mode,编译项目时会抛出警告。 不同的模式,会起到不同的作用,作用如下(官方文档摘取): | 选项 | 描述 | | ----------- | ------------------------------------------------------------ | | development | 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development. 为模块和 chunk 启用有效的名。 | | production | 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePlugin,FlagIncludedChunksPlugin,ModuleConcatenationPlugin,NoEmitOnErrorsPlugin 和 TerserPlugin 。 | | none | 不使用任何默认优化选项 | 我们可以看到 production 模式相比 development 模式启用了更多的插件,在这里,你可以简单认为 production 模式就是自动帮你做了压缩代码和混淆代码功能。 设置mode值的方法有以下方法 - 直接在webpack的配置文件中设置 ```js module.exports = merge(common, { mode: 'development', // production } ``` - 在package.json文件执行webpack脚本命令时设置,比如 ```json "dev": "webpack-dev-server --config ./config/webpack.dev.js --mode=development" ``` 把两个设置mode的地方都不设置,直接执行命令`npm run dev`会出现如下的警告 ``` Compiled with problems: × WARNING 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/ ``` 不管你通过哪种方式设置了webpack的mode之后,webpack会在运行环境添加一个环境变量`process.env.NODE_ENV`,其值就是设置的mode的值,这点可以在入口index.js中得到验证,比如在此文件中添加下面的一行代码 ```js console.log(process.env.NODE_ENV) ``` 需要注意的是,webpack在浏览器这种运行环境只添加了process.env.NODE_ENV这一个变量,没有process以及process.env这两个变量,所以单独访问这两个变量其值是`undefined` 上面设置mode的方式会导致webpack编译处理之后才添加环境变量`process.env.NODE_ENV`,在webpack的配置文件里是无法获取`process.env.NODE_ENV`变量值的。所以,在配置文件里添加代码`console.log(process.env.NODE_ENV)`是不会输出你设置的mode值的 那有没有办法设置一个环境变量,在webpack配置文件以及项目中的代码中都能正确使用呢?答案是可以的 在操作系统层面设置环境变量,前面nodejs章节已经讲过,这里讲另外一种方式,就是在package.json文件中编写,原理还是操作系统设置环境变量的知识,实现方式如下 ```js "scripts": { "dev": "set NODE_ENV=development&&webpack-dev-server --config ./config/webpack.dev.js ", } ``` > 设置时一定要留意$$之前是否有多余的空格字符,否则NODE_ENV的值不是development而是development加空格的情况 设置之后,,在webpack的配置文件就可以使用这个全局的环境变量了,比如下面的代码依据环境变量值设置mode的值 ```js mode: process.env.NODE_ENV === 'production' ? 'production' : 'development' ``` 上面的方式等价于不在package.json中进行环境变量的配置,比如保留下面这样的配置 ```json "scripts": { "dev": "webpack-dev-server --config ./config/webpack.dev.js ", } ``` 然后在运行`npm run dev`命令之前,先在终端设置一个环境变量值,比如你的终端是powershell的话,就执行下面的命令创建环境变量 ```powershell $env:NODE_ENV="development" ``` ### cross-env 不同的终端,不同的平台设置环境变量的写法都不一样,你可以利用cross-env这个库来解决这个问题 cross-env使得您可以使用单个命令,而不必担心为平台正确设置或使用环境变量。 只要在POSIX系统上运行就可以设置好,而cross-env将会正确地设置它。 首先需要安装这个库 ```powershell npm install --save-dev cross-env ``` 接着在package.json文件中进行配置即可,注意下面是没有&&符号以及有一个空格的,环境变量`NODE_ENV`的值仍然是`production`不会多一个空格的 ```json "scripts": { "dev": "cross-env NODE_ENV=production webpack-dev-server --config ./config/webpack.dev.js ", } ``` 如果要设置多个环境变量,那么按下面的方式进行 ```json "scripts": { "dev": "cross-env NODE_ENV=production abc=123 webpack-dev-server --config ./config/webpack.dev.js ", } ``` 你可以直接在webpack的配置文件中编写下面的代码来读取环境变量的值 ```js console.log(process.env.NODE_ENV) console.log(process.env.abc) ``` 上面的代码中读取abc环境变量的代码在其运行环境是浏览器的js文件中是无效的,比如webpack配置的入口js文件,这是因为process.env是一个nodejs运行环境下的对象 至于process.env.NODE_ENV为什么可以在入口js文件有效,是因为webpack打包的时候已经把这个变量值替换为实际的值了,webpack对这个变量进行了特殊对待处理 如果你想让其在类似入口js文件中使用,可以通过DefinePlugin的方式来实现,比如下面的配置 ```js plugins: [ new DefinePlugin({ __DEV__: process.env.devMode, __VERSION__: JSON.stringify('1.0.0'), abc: process.env.abc, }), ] ``` 这样在入口的js文件中就可以编写下面的代码了 ```js //入口js文件,运行在浏览器环境下 console.log(process.env.NODE_ENV) console.log(abc) ``` ### dotenv-webpack 如果你不同的开发环境有不同的一系列的环境变量,最好的办法就是创建多个环境变量文件,依据环境的不同来加载不同的环境变量文件,并把这些环境变量都通过类似DefinePlugin的形式定义一下,以便业务脚本js之类的文件也可以访问这些环境变量,想实现这样的功能就可以使用dotenv-webpack库了,其安装方法如下 ```powershell npm install dotenv-webpack --save-dev ``` 接着在webpack的配置文件中编写如下代码(省略了不相关的内容) ```js const dotenv = require('dotenv') const DotenvWebpack = require('dotenv-webpack') const envConfigPath = { dev: path.resolve(__dirname, '../config/env/.env.dev'), prod: path.resolve(__dirname, '../config/env/.env.prod'), } module.exports = { plugins: [ new DotenvWebpack({ //path: path.resolve(__dirname, '../.env'), // 配置文件路径 path: envConfigPath[process.env.CURRENT_ENV], // 根据环境配置文件路径 }) ], } //不可以正确输出。除非调用了dotenv.config方法读取了相关配置文件 console.log(process.env.bbb) ``` 在业务脚本中像访问环境变量一样使用他们即可,比如 ```js //入口js文件test.js console.log(process.env.bbb) // 这样是不行的 console.log(bbb) ``` package.json文件中指定`CURRENT_ENV`值就可以决定读取的是哪个环境变量文件了 ```json "scripts": { "test": "cross-env CURRENT_ENV=dev webpack --config ./config/webpack.test.js " }, ``` 执行命令`npm run test` 就会读取`.env.dev`文件,里面bbb的值是456,那么依据业务脚本文件test.js构建出的结果js中的内容是`console.log("456")`,替换了代码`console.log(process.env.bbb)` > https://juejin.cn/post/6917794798732574733 # SPA开发 SPA是Single Page Application的简写,也就是单页应用的意思。在本章中将开发一个小案例,充分利用前面讲过的特性,以便了解这些知识是如何结合在一起来开发项目的,本案例的技术点有如下一些 - 支持多环境配置 - 支持小图片变为base64编码 - 支持大图片 - 支持不被webpack管理的文件部署发布 - 支持sass或less样式 - es6代码的转换为es5 - 支持jquery库的使用 - 开发服务器的使用 - css与js的压缩优化 > https://blog.csdn.net/yuyuking/article/details/121750435 ## 目录结构规划 目录结构主要是依据vue官方推荐的目录结构进行了一些改进,参考了如下几个网址的资料 > https://juejin.cn/post/6930533407441027079,https://juejin.cn/post/6986814340196204558,https://juejin.cn/post/7053455302041010189 约定的目录结构如下: ```sh ├── public/ # 静态资源目录 ├── src/ │ ├── assets/ # 全局资源目录 │ │ ├── fonts/ │ │ └── images/ │ │ │ ├── components/ # 全局组件 │ │ └── UserSelectTable/ │ │ ├── style/ │ │ │ ├── _var.scss │ │ │ └── index.scss │ │ ├── UserSelectTable.vue │ │ └── index.js │ │ │ ├── layouts/ # 自定义布局目录 │ │ ├── Basic.vue │ │ └── User.vue │ │ │ ├── mocks/ # 本地模拟数据 │ │ ├── data/ │ │ ├── setup.mock.js │ │ └── users.mock.js │ │ │ ├── store/ # 状态管理 │ │ ├── plugins/ │ │ │ ├── persist.js │ │ │ └── qiankun.js │ │ ├── modules/ # 除非业务过于复杂,否者不推荐 │ │ │ ├── cart.js │ │ │ └── products.js │ │ ├── getters.js # 根级别的 getters │ │ ├── actions.js # 根级别的 action │ │ ├── mutations.js # 根级别的 mutation │ │ └── index.js │ │ │ ├── router/ │ │ ├── routes.js # 路由配置 │ │ └── index.js │ │ │ ├── services/ # 全局数据请求 │ │ │ ├── views/ # 页面级组件 │ │ ├── Home/ │ │ │ ├── components/ # 页面级组件 │ │ │ ├── services/ # 页面级组数据请求 │ │ │ │ └── repo.js │ │ │ └── Home.vue │ │ │ │ │ └── About/ │ │ ├── components/ │ │ └── About.vue │ │ │ ├── login.js # 登录页入口 │ └── main.js # 应用入口 │ ├── .browserslistrc ├── .env ├── .editorconfig ├── .eslintrc.js ├── .prettierrc ├── babel.config.js ├── vue.config.js ├── LICENSE.md ├── jsconfig.json └── package.json ``` ### public 静态资源目录,这里的内容不会被编译,直接复制到编译输出目录。 详见 [HTML 和静态资源](https://link.juejin.cn?target=https%3A%2F%2Fcli.vuejs.org%2Fzh%2Fguide%2Fhtml-and-static-assets.html) ### assets 全局资源目录,这里存放源码中使用到的静态资源,会和源码一起被编译。可能被合并到一个文件中,然后进行压缩。多用来存放业务级的js、css等,如一些全局的scss样式文件、全局的工具类js文件等 ### components 项目通用的组件目录,推荐的目录形式如下: ```sh:no-line-numbers numbers复制代码components/ └── UserSelectTable/ ├── style/ # 组件样式 │ ├── _var.scss │ └── index.scss ├── UserSelectTable.vue └── index.js # 组件出口 ``` 一个组件一个目录,方便后期迁移至出项目,独立成一套业务 UI 组件库。 ### layouts 布局组件目录 ```sh:no-line-numbers numbers复制代码layouts/ ├── Basic.vue # 经典布局 └── User.vue # 用户布局,只有顶部菜单,没有侧边栏菜单 ``` ### mocks 本地模拟数据的目录,详见 [本地 Mock 方案](https://juejin.cn/post/7051078268732047390) 说明。 推荐的目录形式如下: ```sh:no-line-numbers numbers复制代码mocks/ ├── setup.mock.js # Mock 配置 ├── search.mock.js # Mock API └── data/ # 模拟数据目录,存储 json 等形式的文件 ``` ### store [vuex](https://link.juejin.cn?target=https%3A%2F%2Fvuex.vuejs.org%2F) 目录,推荐的目录形式如下: ```sh:no-line-numbers numbers复制代码store/ ├── index.js ├── actions.js # 根级别的 action ├── getters.js # 根级别的 getters ├── mutations.js # 根级别的 mutation ├── plugins/ # 插件目录 │ ├── persist.js │ └── qiankun.js └── modules/ # 除非业务过于复杂,否者不推荐 ├── cart.js └── products.js ``` 如果业务过于负责,可以考虑 MPA 或 微前端 技术。 ### services `services` 会作为项目的数据请求目录,`api` 将从项目中移除,同时如果后端提供了 `swagger` 文档,将使用 [pont](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Falibaba%2Fpont) 自动生成 JS 代码。 ```sh:no-line-numbers numbers复制代码├── services/ # 全局数据请求 │ └── repo.js │ ├── views/ │ ├── services/ # 页面级组数据请求 │ │ │ └── repo.js │ │ └── Home.vue ``` ### router [vue-router](https://link.juejin.cn?target=https%3A%2F%2Frouter.vuejs.org%2F) 目录,推荐的目录形式如下: ```sh sh复制代码router/ ├── routes.js # 路由配置,如果路由过多,建议拆成多个配置文件 └── index.js ``` 通过 [路由配置](https://juejin.cn/post/7053448356156145701) 了解更多。 ### main.js 项目的入口文件,用于对应用进行全局配置,详见 [应用入口](https://juejin.cn/post/7053452468822212644)。 ### .env 环境变量文件,详见 [模式和环境变量](https://link.juejin.cn?target=https%3A%2F%2Fcli.vuejs.org%2Fzh%2Fguide%2Fmode-and-env.html) ### .browserslistrc 在不同前端工具之间共享目标浏览器和 Node.js 版本的配置。详见 [browserslist](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fbrowserslist%2Fbrowserslist)。 在线查询工具 - [browserl.ist](https://link.juejin.cn?target=https%3A%2F%2Fbrowserl.ist%2F) - [browserslist.dev](https://link.juejin.cn?target=https%3A%2F%2Fbrowserslist.dev%2F) ### .editorconfig 统一编辑器的缩进等配置,详见 [EditorConfig](https://link.juejin.cn?target=https%3A%2F%2FEditorConfig.org) 官网。 ### .prettierrc 代码美化工具,详见 详见 [Prettier](https://link.juejin.cn?target=https%3A%2F%2Fprettier.io%2F) 官网。 ### .eslintrc.js 代码检查工具,详见 详见 [ESLint](https://link.juejin.cn?target=https%3A%2F%2Feslint.org%2F) 官网。 ### babel.config.js 语法转换工具,将最新的 JS API 降级为低版本浏览器兼容的代码,详见 详见 [Babel](https://link.juejin.cn?target=https%3A%2F%2Fbabeljs.io%2F) 官网。 ### vue.config.js CLI 配置文件,详见 [全局 CLI 配置](https://link.juejin.cn?target=https%3A%2F%2Fcli.vuejs.org%2Fzh%2Fconfig%2F)。 ### jsconfig.json VSCode 支持的配置文件,和 tsconfig.json 互斥,详见 [jsconfig.json](https://link.juejin.cn?target=https%3A%2F%2Fcode.visualstudio.com%2Fdocs%2Flanguages%2Fjsconfig)。 ### tsconfig.json VSCode 支持的配置文件,和 jsconfig.json 互斥,详见 [tsconfig.json 是什么](https://link.juejin.cn?target=https%3A%2F%2Fwww.typescriptlang.org%2Fzh%2Fdocs%2Fhandbook%2Ftsconfig-json.html)。 ### package.json Node 项目的清单文件,用于提供给如 npm 或 yarn 工具使用,详见 [package.json 指南](https://link.juejin.cn?target=http%3A%2F%2Fnodejs.cn%2Flearn%2Fthe-package-json-guide) ## 项目初始化 创建一个目录spa,在此目录里创建出如下的目录结构,接着执行`npm init`进行初始化 ```shell . |-- babel.config.json |-- config | |-- webpack.common.js | |-- webpack.dev.js | `-- webpack.prod.js |-- package-lock.json |-- package.json |-- public | |-- images | | `-- 100.jpg | |-- index.html | `-- js | `-- jquery-1.6.js |-- src | |-- assets | | |-- css | | | |-- demo.css | | | `-- demo.scss | | `-- images | | |-- 1.jpg | | `-- 2.jpg | |-- demo.ts | |-- main.js | `-- myts.ts `-- tsconfig.json ``` > 生成tree目录的方法见附录 ## git管理 前端项目被git管理起来,主要是要记得加readme.md与git ignore文件,前端工程化项目的ignore文件主要是排除以下内容 - os产生的特定文件与文件夹,比如mac下的DS_Store - IDE产生的一些文件与文件夹,比如.vscode - dist文件夹 - node_modules - 工具或库产生的一些日志文件 下面是一个典型的前端工程化忽略文件: ```ignore .DS_Store node_modules/ /dist/ npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln ``` > 你可以随便在github或gitee上找一个著名的前端项目,拷贝其ignore文件即可 > > https://www.cnblogs.com/JackieDYH/p/17634645.html (这里有介绍ignore文件规则) > > https://www.cnblogs.com/songxingzhu/p/13185796.html ## public资源处理 这一环节主要是演示不经过webpack任何处理的资源文件怎么处理,按照推荐的项目目录结构做的话,指的就是放置在`public`目录下得资源,这些资源文件可能是html,js,css,图片,视频,字体文件等等 ### 普通资源处理 普通资源指的是一些不需要被webpack处理,只需要把这些资源拷贝到发行目录就可以了,这点是通过webpack的拷贝插件完成的,基本配置如下 ```js const CopyPlugin = require('copy-webpack-plugin') new CopyPlugin({ patterns: [ { from: './public/js', to: './js' }, { from: './public/images', to: './imgs' }, ], }) ``` 案例中我通过在public/images目录下添加一个100.jpg图片的方式来演示这一个功能,模板html页面像下面这样的方式使用这些资源即可,注意路径是最终拷贝之后的在发行(dist)目录下的目录即可 ```html ``` ### 第三方js库处理 > 在此文https://juejin.cn/post/6844904095019433997中有较详细的在webpack中使用jQuery的方式说明 > > https://juejin.cn/post/6844904190083350542 有对webpack externals的解释说明(https://juejin.cn/post/7001138300178137118这个也可以看看) > > https://masteringjs.io/tutorials/webpack/externals#:~:text=Webpack%20externals%20tell%20Webpack%20to%20exclude%20a%20certain,your%20client-side%20code%20imports%20Vue%20via%20a%20CDN. 这里主要是通过jQuery这种项目中可能用到的第三方库来讲解这个问题,这些第三方库cdn上可能有,也可能没有,通常这些第三方的库不建议被webpack打包到最终的bundle中去,理由有如下几个 - 把第三方库打包到最终的bundle会增大bundle的大小,影响页面加载速度 - 第三方库打包到bundle中去,可能导致第三方库的插件不能正常使用 - 并不能充分利用cdn下载第三方库的性能优势 第三方库如果运行在浏览器环境下,其处理方法很简单,步骤如下 - 利用webpack的拷贝插件把第三方库的相关文件拷贝到发行目录下(如果cdn中没有这些第三方库的话),webpack中的配置与普通资源的拷贝是类似的 - 在模板html页面中直接引入这些拷贝到发行目录下的js库 ```html ``` - 被webpack管理的js文件中直接使用这些库就可以了,比如你在webpack的入口js文件中写下面的代码,因为webpack处理之后的bundle文件是被插入到了模板html页面里,而且是在第三方库的下面,所以可以直接使用,比如下面的代码 ```js $(() => { $('#btnmsg').click(function(){ const msg = $('#txtmsg').val() console.log(msg) $('.msg').html(msg) }) }) ``` ## asset处理 这里的资源处理指的是被webpack管理的一些资源,一般会放置在src/assets目录里,比如webpack可能会对这些这里面的资源进行合并,转换操作等。 下面通过对图片的处理来理解webpack的处理情况,在assets/images目录下放置2个图片1.jpg,2.jpg,其中1.jpg的大小是超过12K的,然后在assets/css目录下编写一个demo.css的样式文件,代码如下 ```css .asset1 { width: 100px; height: 100px; background-size: cover; background-image: url(../images/1.jpg); } .asset2 { width: 100px; height: 100px; background-size: cover; background-image: url(../images/2.jpg); } ``` 然后在index.html页面中这样使用 ```html

资源大于12k

资源 小于12k

``` 在main.js中导入样式文件即可 ```js import './assets/css/demo.css' ``` 这样启动项目之后,页面是可以正常使用这些样式、图片的,但webpack并没有对其进行任何的处理 如果想让webpack对图片进行转换处理,比如小于12k的图片变成base64编码的数据,那么就需要在webpack的配置文件中进行如下的配置 ```js { test: /.(png|jpe?g|gif|webp|svg)$/, type: 'asset', generator: { filename: 'img/[name].[hash:8][ext]', }, parser: { // 生成Data URI 的条件 dataUrlCondition: { // 默认当资源模块不超过 4kb 时,生成 DataURI,超过 4kb 时,单独打包成文件 maxSize: 13 * 1024, // 14kb }, }, }, ``` 配置好之后,再次启动项目,你就会发现两个样式变成下面这样了,可以看到webpack把图片放置到webpack配置的img目录下了,大于12k的仍然是一个单独的图片文件,而小于12k的变成了base64编码的数据,这样处理的好处就是减少了一次对图片请求的额外http请求。 ```css .asset1 { width: 100px; height: 100px; background-size: cover; background-image: url(http://localhost:3000/img/1.27e4bfeb.jpg); } .asset2 { width: 100px; height: 100px; background-size: cover; background-image: url(data:image/jpeg;base64,省略掉一些数据); } ``` ## 样式处理 普通的css文件功能有限,比如不支持一些编程特性如变量等,写起来繁琐,所以就出现了一些增强型的编写样式的语言,典型的就是sass与less ### 引入sass Sass 文件后缀为 **.scss**或者sass都是可以的。假设有下面的sass文件 ```scss $myColor: blue; h3 { color: $myColor; } input { color: $myColor; } ``` 在入口js中文件引入样式即可使用上了 ```js import './assets/css/demo.scss' ``` 要想让sass变成css以便浏览器识别,是需要经过一定处理的,通常会用三个loader(sass-loader,css-loader,style-loader)去处理 首先,你需要安装 `sass-loader`,`sass-loader` 需要预先安装 [Dart Sass](https://github.com/sass/dart-sass) 或 [Node Sass](https://github.com/sass/node-sass) ```powershell npm install node-sass sass-loader -D --registry=https://registry.npmmirror.com ``` 之后你就可以在webpack配置化文件中进行loader的相关配置,比如下面 ```js { test: /\.s[ac]ss$/i, use: [ // 将 JS 字符串生成为 style 节点 'style-loader', // 将 CSS 转化成 CommonJS 模块 'css-loader', // 将 Sass 编译成 CSS 'sass-loader', ], } ``` ### 后置处理 如果你想对sass进行后置处理,主要做以下几件事 - 安装postcss相关的内容 - 编写postcss的配置文件 - webpack中使用post-loader 安装 ```powershell npm i -D postcss postcss-loader postcss-preset-env ``` 配置文件 ```js //postcss.config.js module.exports = { plugins: [ [ 'postcss-preset-env', { // 其他选项 }, ], ], } ``` 使用 ```js { test: /\.s[ac]ss$/i, use: [ // 将 JS 字符串生成为 style 节点 'style-loader', // 将 CSS 转化成 CommonJS 模块 'css-loader', // 对css进行后置处理,比如添加前缀 'postcss-loader', // 将 Sass 编译成 CSS 'sass-loader', ], } ``` ## 脚本处理 ### ts处理 ts文件的代码如下,由于jQuery库是一个外部资源,所以下面采用对$进行预先类型声明即可,后续就可以直接使用这个jQuery库的全局函数了 ```typescript //demo.ts文件 declare var $: any export default class Demo { public display() { const msg = $('#txtmsg').val() $('.msg').html(msg) } } ``` 在main.js的使用方法如下 ```js //main.js文件 import './assets/css/demo.scss' import $ from 'jquery' import Demo from './demo.ts' $(() => { $('#btnmsg').click(new Demo().display) }) ``` > https://www.jianshu.com/p/e3d19158b5c2 可以参考这篇文章中关于js与ts互相使用的问题 > > https://www.cnblogs.com/xch-jiang/p/14261235.html ts中使用jQuery的问题 ### babel转义js 这里基本与babel-loader章节内容一致,主要是把main.js文件及其引入的内容进行转换,babel的配置文件与webpack的配置内容是一样的 babel.config.json ```json { "presets": ["@babel/preset-env"] } ``` webpack ```js { test: /\.js$/, //排除掉node_module目录 exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', }, } ``` ## 部署优化 部署优化主要是针对生产发布时要进行的一些处理,比如文件的合并压缩等操作就属于部署优化的工作,主要目的是提供性能以便有更好的用户体验,本节讲的一些配置全部都是放置在webpack.prod.js文件里 ### gzip压缩 主要是靠Compression-webpack-plugin插件实现的,核心代码如下 ```js const CompressionPlugin = require('compression-webpack-plugin') plugins: [new CompressionPlugin()], ``` ### 优化与压缩css 优化压缩主要采用的是前面章节讲过的CssMinimizerWebpackPlugin与terser-webpack-plugin插件实现的,核心内容如下 ```js const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') const TerserPlugin = require('terser-webpack-plugin') plugins: [...], optimization: { minimizer: [new CssMinimizerPlugin(), new TerserPlugin()], } ``` ## 多环境 多环境主要针对开发环境与生产环境不同要求就采用不同的配置文件,一般是建立3个配置文件 - webpack.common.js:通用配置,dev与prod都使用 - webpack.dev.js:开发环境配置 - webpack.prod.js:生产环境配置 这种多配置文件与不同环境变量处理,通常会需要两个模块:webpack-merge与cross-env,如果不使用cross-evn,三个配置文件内容如下 ### webpack.common.js ```js const HtmlPlugin = require('html-webpack-plugin') const path = require('path') const CopyPlugin = require('copy-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { entry: './src/main.js', output: { path: path.resolve(__dirname, '../dist'), filename: 'bundle.[contenthash].js', clean: true, }, module: { rules: [ { test: /\.css$/, // css提取插件不再需要style-loader,注意不要加引号 use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'], }, { test: /\.ts$/, use: ['ts-loader'], }, { test: /.(png|jpe?g|gif|webp|svg)$/, type: 'asset', generator: { filename: 'img/[name].[hash:8][ext]', }, parser: { // 生成Data URI 的条件 dataUrlCondition: { // 默认当资源模块不超过 4kb 时,生成 DataURI,超过 4kb 时,单独打包成文件 maxSize: 13 * 1024, // 14kb }, }, }, { test: /\.s[ac]ss$/i, use: [ // 将 JS 字符串生成为 style 节点 'style-loader', // 将 CSS 转化成 CommonJS 模块 'css-loader', 'postcss-loader', // 将 Sass 编译成 CSS 'sass-loader', ], }, { test: /\.js$/, exclude: /(node_modules|bower_components)/, //排除掉node_module目录 use: { loader: 'babel-loader', }, }, ], }, plugins: [ new HtmlPlugin({ template: './public/index.html', title: 'spa with jquery', }), new CopyPlugin({ patterns: [ { from: './public/js', to: './js' }, { from: './public/images', to: './images' }, ], }), new MiniCssExtractPlugin({ filename: 'css/[name].css', }), ], externals: { // jquery: 'jQuery', jquery: 'window.$', }, } ``` ### webpack.dev.js ```js const { merge } = require('webpack-merge') const common = require('./webpack.common.js') const path = require('path') module.exports = merge(common, { mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', devServer: { static: { directory: path.join(__dirname, '../dist'), }, open: true, // 直接打开 port: 3000, hot: true, }, }) ``` ### webpack.prod.js ```js const { merge } = require('webpack-merge') const common = require('./webpack.common.js') const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') const TerserPlugin = require('terser-webpack-plugin') const CompressionPlugin = require('compression-webpack-plugin') module.exports = merge(common, { mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', plugins: [new CompressionPlugin()], optimization: { minimizer: [new CssMinimizerPlugin(), new TerserPlugin()], }, }) ``` ## SPA小结 到此阶段,已经基本完成了一个工程化的SPA应用的开发,总结一下此项目的内容有 - 目录结构 - 配置文件 - 唯一的html页面 - 入口main.js文件 - assets资源 - public的资源 - git的ignore文件 ### 项目目录结构 整个项目的目录结构如下,其中dist目录是build时生成出来的 ```shell . |-- babel.config.json |-- config | |-- webpack.common.js | |-- webpack.dev.js | `-- webpack.prod.js |-- dist | |-- bundle.c065c537b627a75e1978.js | |-- bundle.c065c537b627a75e1978.js.gz | |-- css | | |-- main.css | | `-- main.css.gz | |-- images | | `-- 100.jpg | |-- img | | `-- 1.27e4bfeb.jpg | |-- index.html | |-- index.html.gz | `-- js | |-- jquery-1.6.js | |-- jquery-1.6.js.LICENSE.txt | |-- jquery-1.6.js.LICENSE.txt.gz | `-- jquery-1.6.js.gz |-- package-lock.json |-- package.json |-- postcss.config.js |-- public | |-- images | | `-- 100.jpg | |-- index.html | `-- js | `-- jquery-1.6.js |-- src | |-- assets | | |-- css | | | |-- demo.css | | | `-- demo.scss | | `-- images | | |-- 1.jpg | | `-- 2.jpg | |-- demo.ts | |-- main.js | `-- myts.ts `-- tsconfig.json ``` ### index.html ```html <%= htmlWebpackPlugin.options.title %>

外部资源文件

资源大于12k

资源 小于12k


利用把文本框的内容显示在div里

``` ### demo.css ```css .asset1 { width: 100px; height: 100px; background-size: cover; background-image: url(../images/1.jpg); } .asset2 { width: 100px; height: 100px; background-size: cover; background-image: url(../images/2.jpg); } ``` ### demo.scss ```scss $myColor: red; h3 { color: $myColor; } input { color: $myColor; } /* 这个样式是用来演示postcss效果的,让label标签不可以被选中*/ label { user-select: none; } ``` ### main.js ```js import './assets/css/demo.scss' import './assets/css/demo.css' import $ from 'jquery' import Demo from './demo.ts' $(() => { $('#btnmsg').click(new Demo().display) }) ``` ### demo.ts ```typescript declare var $: any export default class Demo { public display() { const msg = $('#txtmsg').val() $('.msg').html(msg) } } ``` ### babel.config.json ```json { "presets": ["@babel/preset-env"] } ``` ### postcss.config.js ```js module.exports = { plugins: [ [ 'postcss-preset-env', { // 其他选项 }, ], ], } ``` ### ts.config.json ```json { "compilerOptions": { "target": "es5" // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT' }, "exclude": ["./node_modules"] } ``` ### gitignore ```gitignore .DS_Store node_modules/ /dist/ npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln ``` ### package.json ```json { "dependencies": { "qs": "^6.4.1" }, "scripts": { "build": "webpack --config ./config/webpack.dev.js", "dev": "set NODE_ENV=development&&webpack-dev-server --config ./config/webpack.dev.js ", "prod": "set NODE_ENV=production&&webpack --config ./config/webpack.prod.js" }, "devDependencies": { "@babel/core": "^7.23.6", "@babel/preset-env": "^7.23.6", "babel-loader": "^9.1.3", "compression-webpack-plugin": "^10.0.0", "copy-webpack-plugin": "^11.0.0", "cross-env": "^7.0.3", "css-loader": "^6.8.1", "css-minimizer-webpack-plugin": "^5.0.1", "html-webpack-plugin": "^5.5.3", "less": "^4.2.0", "less-loader": "^11.1.3", "mini-css-extract-plugin": "^2.7.6", "node-sass": "^9.0.0", "postcss": "^8.4.32", "postcss-loader": "^7.3.3", "postcss-preset-env": "^9.3.0", "sass-loader": "^13.3.2", "style-loader": "^3.3.3", "ts-loader": "^9.4.4", "webpack": "^5.88.1", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1", "webpack-merge": "^5.10.0" } } ``` ## 支持Vue 本章的主要目的是用纯粹的webpack加上各种包来打造出一个类似于用vue-cli构建出来的项目,以便更好的理解vue工程化项目骨架知识 为了与上面的SPA区分开来,vue相关的文件名都加了一个`-vue`,比如main-vue.js,除非是一些明显属于vue内容的文件就没有加`-vue` > https://juejin.cn/post/7218098727608025144 ### 处理Vue文件 vue文件主要是靠vue-loader去处理的,所以需要安装下面的2个包,其中vue是运行环境需要的包,vue-loader是开发环境所需要的包 ```powershell npm i -S vue --registry=https://registry.npmmirror.com npm i -D vue-loader --registry=https://registry.npmmirror.com ``` > 卸载安装的包 npm uninstall xxx 或者npm npm un xxx > > npm un vue vue-router vuex vue-loader vue-template-compiler vue-style-loader webpack的配置文件如下 ```js //webpack.vue.config.js const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') const { VueLoaderPlugin } = require('vue-loader') module.exports = { mode: 'development', entry: './src/main-vue.js', output: { path: path.resolve(__dirname, '../dist'), filename: 'bundle.[contenthash].js', clean: true, }, devServer: { static: { directory: path.join(__dirname, '../dist'), }, open: true, // 直接打开 port: 3000, hot: true, }, module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'] }, { test: /\.vue$/, use: ['vue-loader'], }, ], }, plugins: [ new HtmlWebpackPlugin({ template: './public/index-vue.html', title: 'spa with vue', }), new VueLoaderPlugin(), ], } ``` package.json中的scripts段的配置如下 ```json "scripts": { "dev-vue": "webpack-dev-server --config ./config/webpack.vue.config.js " } ``` index-vue.html内容如下 ```html your own vue-cli
``` main-vue.js内容如下 ```js import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.mount('#app') ``` app.vue文件内容如下,此文件在src目录下 ```vue ``` ### Sass样式 如果想在vue组件中编写sass样式,比如下面这样 ```vue ``` 那么安装相关的sass处理loader就可以支持了,方法跟前面一样,接着在配置文件中进行如下的配置(记得删掉css的规则匹配) ```js { test: /\.(scss|sass)$/, use: ['style-loader', 'css-loader', 'sass-loader'], } ``` ### Vue Router 要支持路由首先需要安装vue-router,安装完之后,先把app.vue文件的template内容改成下面这样,app.vue文件的其它地方不变 ```vue ``` 接着在src下建立views目录,在views目录建立两个文件Home.vue与About.Vue,内容分别为 ```vue ``` about.vue ```vue ``` 接着在src下建立router目录,在此目录建立index.js文件,就是在这个文件中来配置路由与各个vue文件的对应关系的,也就是管理路由的 ```js import { createRouter, createWebHistory } from 'vue-router' import Home from '../views/Home.vue' const routes = [ { path: '/', name: 'Home', component: Home, }, { path: '/about', name: 'About', component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'), }, ] const router = createRouter({ history: createWebHistory(), routes, }) export default router ``` 上面导入视图用来两种方式,并且采用的是历史记录而不是hash的方式来处理路由,所以最好在开发服务器配置上下面的选项(我所用的开发服务器不配置也有效果) ```js //webpack配置文件里 devServer: { static: { directory: path.join(__dirname, '../dist'), }, open: true, // 直接打开 port: 3000, hot: true, historyApiFallback: true, } ``` 最后在main-vue.js文件中引入router目录下的index.js文件并配置上路由,代码如下 ```js import { createApp } from 'vue' import App from './App.vue' import router from './router' const app = createApp(App) // 挂载上路由 app.use(router).mount('#app') ``` ### Vuex vuex是一个专为 Vue.js 应用程序开发的**状态管理模式**, 采用**集中式存储**管理应用的所有组件的状态,解决多组件数据通信。(简单来说就是管理数据的,相当于一个仓库,里面存放着各种需要共享的数据,所有组件都可以拿到里面的数据) > https://juejin.cn/post/7013325675129995272 要使用上Vuex,首先需要执行下面的命令安装它 ```powershell npm i vuex ``` 在这里我们仍然使用前面的例子,在About页面中修改一个变量的值,在Home页面显示这个变量最新的值,要做得事情有以下一些 - 在src下建立store文件夹,在此文件夹里建立index.js文件用来管理状态存储的逻辑 - 把store相关的内容引入到main-vue.js文件里,并挂载到vue实例里 - 在About里改变变量值,在About里读取变量值 src/store/index.js代码如下 ```js import { createStore } from 'vuex' const store = createStore({ state: { count: 1 }, actions: { add ({ commit }) { commit('add') } }, mutations: { add (state) { state.count++ } }, getters: { getCount (state) { return state.count } } }) export default store ``` src/main-vue.js文件内容如下 ```js import { createApp } from 'vue' import App from './App.vue' //导入了router目录下的index.js import router from './router/index' import store from './store' const app = createApp(App) app.use(router).use(store).mount('#app') ``` Home.vue文件内容如下 ```vue ``` About.vue文件内容如下 ```vue ``` # 附录 ## tree 前提是需要安装Tree for Windows工具: 1. 打开进入 [Tree for Windows](http://gnuwin32.sourceforge.net/packages/tree.htm) 页面,选择下载 Binaries zip 文件。 2. 解压压缩包,找到压缩包内的 bin 目录,将 bin 目录下的 tree.exe 复制 3. 找到 `C:\\Program Files\Git\usr\bin` 目录,将 tree.exe 粘贴到该目录下,安装即完成 ```powershell tree -I node_modules -L 3 ``` ## 常见英文缩写含义 - lint:lint 工具用来检查编程错误,比如缩进,换行,引号等等,确保项目中的写法是一致的,常见的用eslint,jslint等等 - rc:running command ,文件中有rc字样的,基本就是一些程序的运行配置文件,比如zshrc,browserslistrc等 - prettier:通常指代码美化方面的内容 - polyfill:垫片的意思,在开发中通常是指一个框架或者库自动帮你添加一些功能,比如你写的代码用到的一个函数在浏览器20这个版本是有的,但19这个版本没有,这些垫片性质的库会自动帮你生成这个缺少的函数,当你想让你的程序也能在19这个版本运行的话 - dist:distribution缩写,分发,发行,发布的意思 # 参考资料 https://juejin.cn/post/6844904036206919688 (从0搭建一个工程化vue 项目,不用vue cli) https://juejin.cn/post/7137110113277444126 (包安装以及package与package.lock区别) https://juejin.cn/post/7068216285724672008#heading-0(webpack的loader与plugin理解与编写) https://juejin.cn/post/7042597516071665671 (webpack server基本用法) https://juejin.cn/post/7058652098174386213 (webpack loader执行顺序) https://juejin.cn/post/7032918076089696264 (Autoprefixer) https://www.ruanyifeng.com/blog/2016/01/babel.html (babel入门教程) https://juejin.cn/post/6901649054225465352 (理解babel作用) https://www.cnblogs.com/EricZLin/p/9409235.html#:~:text=%E4%BA%8C%E3%80%81webpack%E9%87%8C%E4%BD%BF%E7%94%A8babel%201%201%E3%80%81babel-loader%20babel-core%20babel-preset-env%20%EF%BC%88%E8%BD%AC%E6%8D%A2%E8%AF%AD%E6%B3%95%EF%BC%89,2%202%E3%80%81babel-polyfill%20%E6%9D%A5%E6%8F%90%E4%BE%9B%E4%BD%8E%E7%89%88%E6%9C%AC%E6%B5%8F%E8%A7%88%E5%99%A8%E4%B8%AD%E7%9A%84%E4%B8%8D%E6%94%AF%E6%8C%81API%203%203%E3%80%81transform-runtime%E8%A7%A3%E5%86%B3%E4%BB%A3%E7%A0%81%E9%87%8D%E5%A4%8D%E9%97%AE%E9%A2%98%204%204%E3%80%81%E9%85%8D%E7%BD%AEcacheDirectory%E5%8F%AF%E4%BB%A5%E8%8A%82%E7%9C%81webpack%E6%89%93%E5%8C%85%E7%BC%96%E8%AF%91%E6%97%B6%E9%97%B4%EF%BC%8C%E9%BB%98%E8%AE%A4%E6%8A%8A%E6%89%93%E5%8C%85%E7%9A%84%E7%BB%93%E6%9E%9C%E7%BC%93%E5%AD%98%E5%88%B0node_modules%2F.cache%E6%A8%A1%E5%9D%97%E4%B8%8B (babel在webpack中的基本使用) https://juejin.cn/post/7039919941750882335 (讲解了nodejs的require) https://juejin.cn/post/7091597732107943966 (webpack与webpack-cli) https://juejin.cn/post/7012275438558904350(前端路由与spa单页应用) https://www.cnblogs.com/ricolee/p/cmd-tree.html (windows下使用tree命令) https://juejin.cn/post/6844903493153587213 (基于webpack的静态资源处理)