我正在参加「AdminJS·启航计划」
由于前端技术发展迅速,如今落地一个现代前端项并不容易,其中涉及非常多的知识、工具和经验。在前端领域,学习的层次有两个:一个是以使用者的角度去掌握知识和技能,并融会贯通;而更深层次是从根本原理上彻底理解知识和技能,不仅做到融会贯通,更能达到根据当前应用场景创造最优解的境界。
本系列会从零开始,尝试讲述中大厂前端项目的落地。落地的过程中,一般要考虑项目的稳定性、可维护性、安全性和代码可读性(易理解性),如果正在阅读本系列的你,还对前端架构不太理得清,本系列也许会帮你找到一点头绪。
从零开始的中大厂前端项目落地(一)
落地一个前端项目涉及非常多知识,因此划分为6篇文章,化整为零,逐个击破,以下是各篇文章对应的重点:
选择合适的构建工具
事不宜迟,马上开始吧。从零开始落地一个项目,首先要把地基选好,而我觉得项目的地基不外乎就是项目技术选型,不止是Vue和React或者Antd和Element的选择,还有架构工具、前端工具链、安装机制等的选型。接下来通过以下三个方面逐一讲述:
- 安装机制,私服部署原理
- 技术体系解析
- 主流架构工具的设计 & 选择打包工具
安装机制,私服部署原理
从零开始一个项目,大家第一步是不是会在命令行输入npm init
或者yarn init
,那究竟用npm
还是yarn
?
现在我们遇到了第一个分岔路口,但是不用担心,选择任何一个最终都会到达目的地。
接下来分别看看npm
和yarn
的机制。
npm
npm
的安装机制非常值得深究,它会优先安装依赖包到当前项目目录,使得不同应用项目的依赖各成体系,同时还能减轻包作者的API兼容压力性,但是这样做的缺陷也很明显:如果项目A和项目B都依赖相同的公共库C(依赖C的版本也一致),那么公共库C会在项目A和B中各被安装一次。
那npm
是怎么将要依赖的库下载到本地的呢,下面是npm
的安装机制
在命令行输入npm install
,会先检查 config,获取 npm 配置信息,
然后检查项目中有无 package-lock.json 文件。
如果有package-lock.json 文件,则检查 package.json 和 package-lock.json 文件中声明的版本是否一致。
- 一致的话直接使用 package-lock.json 中的信息,从缓存或者网络资源中加载依赖;
- 不一致则根据 npm 版本进行处理(具体如上图)
如果没有package-lock.json 文件,则根据 package.json 文件递归建立依赖树,然后按照构建好的依赖树下载依赖资源,并且下载的时候会检查是否有缓存。
- 有缓存的话直接解压到 node_modules ;
- 没有缓存,则先从 npm 远程仓库下载资源,加压到 node_modules ,并且将其添加到缓存。
构建依赖书时,当前依赖项目无论是直接依赖还是子依赖的依赖 ,我们都应该遵循扁平化原则优先将其放在 node_modules 目录下, 如果遇到相同模块应先判断已放置在依赖树中的模块版本是否符合对新模块版本的要求。
接下来我们一起用 npm
创建一个项目(用的是npm 6.14.18)
//项目初始化
npm init
//使用默认设置初始化项目
npm init -y
可以看到项目的文件夹多了一个 package.json 文件,
打开可以看到一些默认的信息
但是 package.json 能配置的字段远远不止上面这些,以下是从官网找到的配置项:
字段 | 描述 | 解析 |
---|---|---|
Description | 描述 | - |
name | 项目名称 | - |
version | 版本号 | - |
description | 项目描述 | - |
keywords | 关键词 | 把关键字放进去。这是一个字符串数组。这有助于人们在npm搜索中发现您的包 |
homepage | 主页 | 项目主页的url |
bugs | 漏洞 | 项目问题跟踪器的url和/或应报告问题的电子邮件地址。这些对遇到包裹问题的人很有帮助 |
license | 许可证、协议 | 开源协议有GPL、LGPL、MIT、BSD、Apache,一般有lICENSE文件 |
files | 文件模式 | 描述作为依赖项安装包时要包含的条目 |
main | 入口 | 程序的主要入口点 |
browser | 浏览器 | 提示用户在浏览器环境使用 |
bin | 可执行文件 | - |
directories | 目录 | - |
repository | 储存库 | 指定代码所在的位置 |
scripts | 脚本 | - |
config | 脚本的配置 | 可用于设置包脚本中使用的配置参数 |
dependencies | 项目依赖 | 线上生产环境中的项目依赖 |
devDependencies | 开发依赖 | 开发阶段起作用或是只能用于开发环境中被用到的项目依赖 |
peerDependencies | 同版本的依赖 | 开发开源库时用到的对应的项目依赖 |
bundledDependencies | 捆绑依赖 | - |
optionalDependencies | 可选依赖 | - |
尽管 npm
越来越成熟,但是还是解决不了公共库C会在项目A和B中各被安装一次的问题。这时候monorepo
很好的帮我们解决了这部分问题。multierpo
就是将引用按照模块分别在不同的产库中进行管理,而monorepo
就是将应用中所有模块全部放在同一个项目中,这样不需要单独发包、测试,且所有代码都在一个项目中管理,一同部署上线,能够在开发阶段更早地复现bug,暴露问题。这就是项目代码在组织上的不同哲学:一种提倡分而治之
,一种提倡集中管理
。究竟选择哪种组织,就要根据团队风格和实际场景进行选型了。
yarn
接下来我们看看用 yarn
创建项目和 npm
有何不同
//项目初始化
yarn init
//安装项目的全部依赖
yarn install
可以发现,和 npm init
一样,执行完命令后,文件夹也是有一个 package.json 文件
现在的 yarn
和 npm(version:5+)
区别不大,这里就不展开细讲了(老墨,我想摸鱼了
)
pnpm
npm2 是通过嵌套的方式管理 node_modules 的,会有同样的依赖复制多次的问题。
npm3+ 和 yarn 是通过铺平的扁平化的方式来管理 node_modules,解决了嵌套方式的部分问题,但是引入了幽灵依赖的问题,并且同名的包只会提升一个版本的,其余的版本依然会复制多次。
pnpm 则是用了另一种方式,不再是复制了,而是都从全局 store 硬连接到 node_modules/.pnpm,然后之间通过软链接来组织依赖关系。
这样不但节省磁盘空间,也没有幽灵依赖问题,安装速度还快,从机制上来说完胜 npm 和 yarn。
技术体系解析
在传统体系中,通过script标签引入 Javascript 文件,这种体系下每个依赖库都要提供一个 .js 格式的文件,如果想要使用一个依赖库,就必须在使用之前手动引入,随着前端项目规模的扩大,将依赖关系交给开发者手动维护对开发者非常不友好。
随着前端工程化的发展,前端构建工具已经成为项目标配。
CJS
与ESM
有何细微不同。
- | ESM | cjs |
---|---|---|
语法类型 | 静态 | 动态 |
关键声明 | export、import | require、module.export |
加载方式 | 编译时加载 | 运行时加载 |
加载行为 | 异步加载 | 同步加载 |
书写位置 | 顶部位置 | 任何位置 |
指针指向 | this指向underfind | this指向当前模块 |
执行顺序 | 引用时生成只读引用 执行是才 正式取值 | 首次引用时加载模块 再次引用时 读取缓存 |
属性应用 | 所有类型属于动态只读引用 | 基本类型属于复制不共享 引用类型属于 浅拷贝且共享 |
属性修改 | 工作空间不可修改引用的值 但可以通过引用的方法修改 | 工作空间可修改引用的值 |
node变量 | 不支持 | 支持 例如:__filename |
主流架构工具的设计
从零到一的前端架构离不开构建工具的加持,对构建工具的理解、选择和应用决定了我们是否能够打造一个使用顺畅且接近完美的产品。
对于构建工具,目前给到前端的选择是Webpack、Rollup、Vite或者Turbopack,相信读者都不陌生。而我们这次从零到一的项目选择 Webpack ,主要构建工具太多,一个个细讲会耗费大量的篇幅,所以挑了一个比较具代表性的 Webpack。
webpack
webpack 是一种模块打包工具,可以将各类型的资源,例如图片、CSS、JS 等,转译组合为 JS 格式的 bundle 文件。
webpack 构建的核心任务是完成内容转化和资源合并。主要包含以下 3 个阶段:
-
初始化阶段
-
构建阶段
- 开始编译:执行 Compiler 对象的 run 方法,创建 Compilation 对象。
- 确认编译入口:进入 entryOption 阶段,读取配置的 Entries,递归遍历所有的入口文件,调用 Compilation.addEntry 将入口文件转换为 Dependency 对象。
- 编译模块(make) : 调用 normalModule 中的 build 开启构建,从 entry 文件开始,调用 loader 对模块进行转译处理,然后调用 JS 解释器(acorn)将内容转化为 AST 对象,然后递归分析依赖,依次处理全部文件。
- 完成模块编译:在上一步处理好所有模块后,得到模块编译产物和依赖关系图。
-
生成阶段
- 输出资源(seal) :根据入口和模块之间的依赖关系,组装成多个包含多个模块的 Chunk,再把每个 Chunk 转换成一个 Asset 加入到输出列表,这步是可以修改输出内容的最后机会。
- 写入文件系统(emitAssets) :确定好输出内容后,根据配置的 output 将内容写入文件系统。
Webpack
作为一个模块打包工具,本身就可以解决模块化代码打包的问题,将零散的 JavaScript 代码打包到一个或多个 JS
文件中。
对于有环境兼容问题的代码,Webpack
可以在打包过程中通过 Loader
机制对其实现编译转换,然后再进行打包
。
对于不同类型的前端模块类型,Webpack
支持在 JavaScript 中以模块化的方式载入任意类型的资源文件,例如,我们可以通过 Webpack 实现在 JavaScript 中加载 CSS 文件,被加载的 CSS 文件将会通过 style 标签的方式工作。
除此之外,Webpack
还具备代码拆分
的能力,它能够将应用中所有的模块按照我们的需要分块打包
。这样一来,就不用担心全部代码打包到一起,产生单个文件过大,导致加载慢的问题。我们可以把应用初次加载所必需的模块打包到一起,其他的模块再单独打包,等到应用工作过程中实际需要用到某个模块,再异步加载该模块,实现增量加载,或者叫作渐进式加载,非常适合现代化的大型 Web 应用。
当然,除了 Webpack
,其他的打包工具也都类似,总之,所有的打包工具都是以实现模块化为目标,让我们可以在开发阶段更好的享受模块化带来的优势,同时又不必担心模块化在生产环境中产生新的问题。
接下来我们故技重施,一起来写一下 webpack 吧。回到我们的项目:
npm install webpack webpack-cli --save-dev
然后创建 webpack.config.js , 这里就是放置我们 webpack 的配置
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
};
下面是 webpack 用到的字段
字段 | 描述 | 解析 |
---|---|---|
entry | 入口 | 指示 webpack 应该使用哪个模块 |
output | 输出 | 告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件 |
loader | 版本号 | - |
plugin | 插件 | - |
mode | 模式 | development , production 或 none |
browser compatibility | 浏览器兼容性 | - |
environment | 环境 | - |
到此为止,一个项目的雏形就出现了,同学们是不是已经开始摩拳擦掌想大干一场了?且慢,接下来还要先定制团队代码规范,有了规矩才能成方圆,而至于如何定制,还请看下一篇。
最后,让我们一起加油吧!
参考资料:
docs.npmjs.com/about-npm
yarn.bootcss.com/docs
Admin.net/post/712729…
webpack.docschina.org/concepts/
Admin.net/post/707165…
Admin.net/post/710225…
Admin.net/post/711013…