ZTeam's Elpis Note
KOA
Koa 应用程序是一个包含一组中间件函数
的 对象
ghooks(提交的hooks)
- commit-msg:提交内容的时候去执行的库
- pre-commit:提交之前执行什么
glob
sync
glob.sync() 是一个非常实用的文件匹配模式方法,主要用于文件系统的模式匹配。
基本功能:
根据特定的模式规则(pattern)查找匹配的文件路径
返回一个字符串数组,包含所有匹配的文件路径
sync 表示这是同步版本,会阻塞执行直到完成搜索
const glob = require('glob') // 查找所有 .js 文件 const jsFiles = glob.sync('**/*.js') // 查找特定目录下的所有 .js 文件 const controllerFiles = glob.sync('app/controller/**/*.js') // 排除 node_modules 目录 const files = glob.sync('**/*.js', { ignore: ['node_modules/**'] })
node.js
Path.resolve
是一个node.js的方法,path.resolve() 是 Node.js 中 path 模块提供的一个方法,用于将路径或路径片段的序列解析为绝对路径
。它的工作方式类似于在命令行中 CD(切换目录)命令的执行过程。将后面的路径往前面进行拼凑,例如
path.resolve(process.cwd(), './app/pages')//得到的路径就会是一个绝对路径,xxx/Elpis/app/pages
Path.baseName
p
:表示你要获取文件名的路径字符串,可以是文件的绝对路径或相对路径。
ext
(可选):表示要去掉的扩展名。如果指定了 ext
,返回的文件名将会去掉这个扩展名部分。如果不指定 ext
,则返回文件名带扩展名。
//demo1
const path = require('path');
const filePath = '/user/local/test/file.txt';
const fileName = path.basename(filePath);
console.log(fileName); // 输出: 'file.txt'
//demo2
const path = require('path');
const filePath = '/user/local/test/file.txt';
const fileNameWithoutExt = path.basename(filePath, '.txt');
console.log(fileNameWithoutExt); // 输出: 'file'
Glob.sync
这是一个 glob 模块中的一个同步方法,广泛用于文件匹配和查找。它基于 glob pattern (即通配符模式)来查找文件路径,通常用于获取符合某种模式的文件列表,支持多种通配符匹配规则。
*
:匹配文件名中的任意字符(不包括路径分隔符)。
?
:匹配单个字符。
[]
:匹配字符集合中的一个字符,如 [a-z]
。
**
:匹配任意目录或文件(包括子目录)。
{}
:匹配多种选择,类似正则中的 |
,如 {a,b,c}
匹配 a
、b
或 c
。
//例子:例如这里拿到了bussinessPath这个变量,拼接上/router-schema
const routerSchemaPath = path.resolve(app.businessPath,.${sep}router-schema)
//1、利用path.resolve再次拼接,得到:/businessPath/router-schema/**/**.js
//2、利用glob.sync方法,根据传入的模式(此处是 /**/**.js)同步地查找匹配的文件,所以就会返回匹配的数组列表
const fileList = glob.sync(path.resolve(routerSchemaPath,.${sep}**${sep}**.js))
Babel
学习地址:https://www.jiangruitao.com/babel/
Babel的主要功能
- 给新的JavaScript语法转换成旧的语法,注意是语法不是API
- 做polyFill,为当前环境提供一个垫片,所谓垫片就是垫平不同浏览器之间的差异,让旧版浏览器可以运行新版的API
Babel的配置文件
Babel的配置文件是Babel执行时默认会在当前目录寻找的文件有.babelrc,.babelrc.js,babel.config.js和package.json。它们的配置项都是相同,作用也是一样的,只需要选择其中一种。一般来说它们的配置项其实都是 plugins 和 presets
module.exports = {
"presets": ["es2015", "react"],
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
另外,除了把配置写在上述这几种配置文件里,我们也可以写在构建工具的配置里。对于不同的构建工具,Babel也提供了相应的配置项,例如 webpack 的 babel-loader的配置项
插件(plugin)与预设(preset)
plugin代表插件,preset代表预设,它们分别放在plugins和presets,每个插件或预设都是一个npm包。
Babel插件有很多,假如只配置插件数组,我们的Babel配置文件会非常臃肿,preset预设就是帮我们解决这个问题的。
预设是一组Babel插件的集合 ,用大白话说 预设就是插件包,例如 babel-preset-es2015 就是所有处理es2015的二十多个Babel插件的集合。这样我们就不用写一大堆插件配置项了,只需要用一个预设代替就可以了,预设也可以是插件和其它预设的集合。
Babel的插件 和 预设的参数
每个插件是插件数组的一成员项,每个预设是预设数组的一成员项,默认情况下,成员项都是用字符串来表示的,例如"@babel/preset-env"。 如果要给插件或预设设置参数,那么成员项就不能写成字符串了,而要改写成一个数组。数组的第一项是插件或预设的名称字符串,第二项是个对象,该对象用来设置第一项代表的插件或预设的参数
{
"presets":[
["@babel/preset-env",{
"useBuiltIns": "entry"
}]
]
}
常用的Babel预设与插件
常用的预设:
@babel/preset-env
该预设可以进行语法转换,还可以通过设置参数项进行针对性语法转换以及polyfill的部分引入。重要的参数项就只有以下几个:
browsersList
browsersList也叫目标环境配置表,除了写在package.json也可以写在.browserslistrc文件中。用了browserslist指定要运行在哪些浏览器或者node.js环境之后,浏览器就会自动判断添加CSS前缀例如(‘-webkit-’),如果使用了@babel/preset-env不设置任何参数,Babel就会读取browserslist的配置来做语法转换,如果也没有设置browserslist,那么就回把所有ES6的语法转换为ES5的语法
"browserslist": [ "> 1%", "not ie <= 8" ]
这里的含义为市场份额大于1%的浏览器且不考虑IE8以下的浏览器
targets
targets可以为字符串、字符串数组或者对象,不设置的时候默认为空对象,他的写法和browserslist是一样的。
如果babel/env使用了targets参数,那么就不会读取package.json的browserslist配置。
如果不设置targets就会读取browserslist
如果都没有则@babel/env就会对所有ES6语法转换成ES5的语法
useBuiltIns
useBuiltIns的值为 "usage" | "entry" | false ,默认值为false
useBuiltIns主要与polyfill有关,如果为false,那么polyfill则会全部引入到代码当中,如果设置为
entry
或者usage
则会根据配置找出需要的polyfill进行引入false:全部polyfill进行引入
- entry:需要在入口文件引入polyfill文件
- usage:babel除了会考虑目标环境实际使用到的API模块,还会考虑项目中使用到的ES6实例,只有我们使用到了ES6特性在目标环境中缺失的时候才会引入core-js的API补齐模块
这个时候我们就看出了'entry'与'usage'这两个参数值的区别:'entry'这种方式不会根据我们实际用到的API进行针对性引入polyfill,而'usage'可以做到。另外,在使用的时候,'entry'需要我们在项目入口处手动引入polyfill,而'usage'不需要。需要注意的是,使用 'entry' 这种方式的时候,只能import polyfill一次,一般都是在入口文件。如果进行多次import,会发生错误。
corejs
corejs的值只能为 2 、 3 、false,取值为2的时候,babel转码使用的是core-js@2的版本,如果某些新版API则需要使用core-js@3,
modules
这个参数项的取值可以是"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | "false",在不设置的时候,取默认值" auto"。
我们常见的模块化语法有两种:
(1)ES6的模块法语法用的是import与export;
(2)commonjs模块化语法是require与module.exports;
在该参数项值是 'auto' 或 不设置 的时候,会发现我们转码前的代码里import都被转码成require了。
如果我们将参数项改成false,那么就不会对ES6模块化进行更改,还是使用import引入模块。
使用ES6模块化语法有什么好处呢。在使用Webpack一类的打包工具,可以进行静态分析,从而可以做tree shaking 等优化措施。
@babel/preset-flow
@babel/preset-react
@babel/preset-typescript
常用的插件: 目前比较常用的插件只有 @babel/plugin-transform-runtime
@babel/plugin-transform-runtime 与 @babel/runtime
当我们使用了ES6的一些功能的时候,@babel/present-env,会帮我们在文件中注入一些辅助函数,以便语法转换后可以使用,但是不可能每个文件都加上这么多的辅助函数,所以需要把这些函数声明放到一个npm包里面,在使用的时候直接把这个包引入到文件当中,这样就能做到复用、减少体积。@babel/runtime就是这个包。这个包里面包含许多的辅助函数,但是我们的文件中有可能只需要其中一两个,所以@Babel/plugin-transform-runtime就是用来解决这个问题。
通俗来讲:就是@babel/runtime包提供辅助函数模块,还要安装Babel插件@babel/plugin-transform-runtime来自动替换辅助函数
@babel/plugin-transform-runtime vs babel-polyfill
我们了引入babel-polyfill 或 core-js/stable 与 regenerator-runtime/runtime 来做全局的API补齐。但这样可能有一个问题,那就是对运行环境产生了污染。例如Promise,我们的polyfill是对浏览器的全局对象进行了重新赋值,我们重写了Promise及其原型链。但是如果不想产生污染可以使用@babel/plugin-transform-runtime , 主要的应用场景是JS库和npm包开发的时候,如果开发JS库的人使用polyfill补齐API,我们前端工程也使用polyfill补齐API,但JS库的polyfill版本或内容与我们前端工程的不一致就会导致前端工程问题。
babel常用的工具
@babel/cli,@babel/core与@babel/preset-env是Babel官方的三个包,它们的作用如下:
@babel/cli是Babel命令行转码工具,如果我们使用命令行进行Babel转码就需要安装它。
@babel/cli依赖@babel/core,因此也需要安装@babel/core这个Babel核心npm包。
@babel/preset-env这个npm包提供了ES6转换ES5的语法转换规则,我们在Babel配置文件里指定使用它。如果不使用的话,也可以完成转码,但转码后的代码仍然是ES6的,相当于没有转码。
babel-loader
- babel-loader是用于webpack的一个loader,以便webpack在构建的时候用Babel对JS代码进行转译,这样我们就不用再通过命令行手动转译了。在webpack配置文件中,我们把babel-loader添加到module的loaders列表中
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
在这里,我们通过options属性给babel-loader传递预设和插件等Babel配置项。我们也可以省略这个options,这个时候babel-loader会去读取默认的Babel配置文件,也就是.babelrc,.babelrc.js,babel.config.js等。在现在的前端开发中,建议通过配置文件来传递这些配置项。
技巧
提交校验:ghooks:
- 提交的时候执行validate-commit-msg这个库
- 提交之前运行npm run lint
"ghooks": {
"commit-msg": "validate-commit-msg",
"pre-commit": "npm run lint"
}
通过controller传参给页面,然后页面挂载在全局
view-controller
module.exports = app => {
return class ViewController {
/**
* 渲染页面
* @param {Object} ctx controller上下文
*/
async renderPage(ctx) {
await ctx.render(`output/entry.${ctx.params.page}`, {
name: app.options?.name,
env: app.env.get(),
options: JSON.stringify(app.options)
})
}
}
}
Entry.page1.tpl
<!DOCTYPE html>
<html>
<head>
<title>{{ name }}</title>
</head>
<body>
<h1>this is page1</h1>
<input id="env" value="{{ env }}" style="display:none"/>
<input id="options" value="{{ options }}" style="display:none"/>
</body>
<script type="text/javascript">
try {
window.env = document.getElementById("env").value;
const options = document.getElementById("options").value;
window.options = JSON.parse(options)
} catch (e) {
console.log(e);
}
</script>
</html>
章节笔记:
引擎内核应用:
页面渲染流程
设置环境:通过package.json增加script,给_ENV赋值,执行不同的配置文件
nodemon:热更新
访问页面流程:
调用start,注册中间件
注册controller
javascript class ViewController { /** * 渲染页面 * @param {Object} ctx controller上下文 */ async renderPage(ctx) { await ctx.render(`output/entry.${ctx.params.page}`) } }
javascript app.use(koaNunjucks({ ext:"tpl", path: path.resolve(process.cwd(),'./app/public'),//渲染根目录 nunjucksConfig: { nocache:true, trimBlocks:true } }))当调用renderPage的时候上下文会多一个render属性,把文件路径拼接到渲染引擎路径后面 - 注册全局中间件 - 使用了模版渲染引擎noa-nunjucks-2这个中间件 - 设置渲染tpl路径,/app/public为目录 -
javascript router.get('/view/:page', viewController.renderPage.bind(viewController))- 注册路由 -
当用户访问路由view/page1,触发此路由文件,:page为变量page1,就会去调用对应的controller的renderPage方法,而renderPage方法会访问的文件路径则为 `渲染引擎路径`+`render路径` = `./app/public/output/entry.page1.tpl` 1、路由3、tpl文件 ```2、controller
接口访问流程
1、路由
2、controller:controller一般是服务于业务,它可以调取不同的service
3、service:处理单一业务,因为service不是面向单一controller,而是可供多个controller调用
工程化实现
webpack的配置文件(webpack.config.js)
入口配置(entry)
入口文件,因为是多入口,所以对应了不同的入口文件
entry: {
'entry.page1':'./app/pages/page1/entry.page1.js',
'entry.page2':'./app/pages/page2/entry.page2.js'
}
模块解析器(module)
一般来说是用正则表达式去匹配文件后缀,什么文件用什么loader去解释这个文件,在modules的rules中进行配置,
use的loader中,顺序是按照从右往左执行
module: {
rules: [{
test: /\.vue$/,
use: {
loader: 'vue-loader',
}
},
{
test: /\.js$/,
//include:可以只对某一个路径下的文件才处理
include: [path.resolve(process.cwd(), './app/pages')],//这里的意思是只对业务代码才进行babel的处理
use: {
loader: 'babel-loader',
}
}
]
}
输出路径(output)
output: {
//在js路径下/文件名_8位的哈希值.bundle.js
filename: 'js/[name]_[chunkhash:8].bundle.js',
//输出路径
path: path.join(process.cwd(), './app/public/dist/prod'),//静态资源路径
publicPath:'/dist/prod',
//允许跨域
crossOriginLoading:'anonymous'
}
模块解析行为(resolve)
一般来说,resolve负责处理模块解析的相关行为
resolve: {
//用户在引入的时候,引入到最后的文件时可以不写后缀 例如/demo.js 只写/demo,按照以下的顺序进行匹配
extensions: ['.js', '.vue', '.less', '.css'],
//路径别名
alias: {
$pages: path.resolve(process.cwd(), './app/pages/')
}
}
插件(plugin)
plugins为一个数组,一般来说用于存放插件实例
/**
* 配置webpack插件
*/
plugins: [
/**
* 处理.vue文件这个插件是必须的,他的职能是将你定义过的其他规则复制并应用到 .vue 文件中
* 例如:有一条匹配规则是 /\.js$/ 的规则,那么他会应用到 .vue 文件中的 <script> 板块
*/
new VueLoaderPlugin(),
/**
* 将第三方库暴露到window的context下
* 例如:window.Vue
*/
new webpack.ProvidePlugin({
Vue: 'vue'
}),
/**
* 定义全局变量
*/
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: 'true',//支持 vue 解析optionsApi
__VUE_PROD_DEVTOOLS__: 'false',//禁用 vue 调试工具
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',//禁用生产环境显示 “水合” 信息
}),
//构造最终渲染的页面模版
...htmlWebpackPluginList
],
打包输出(optimization)
做分包,模块划分
- 分包策略:把js打包成三种类型
- vendors: 第三方库,基本不会改动,除非依赖版本升级
- common:业务组件代码的公共部分抽离出来,改动较少
- entry.{page}:不同页面 entry 里面的业务代码,会经常改动
- 目的:动静结合,把改动和引用频率不一样的js区分出来,以达到更好利用浏览器缓存的效果
splitChunks: {
//对同步和异步模块都进行处理
chunks: "all",
//每次异步加载的最大并行请求数
maxAsyncRequests: 10,
//入口点的最大并行请求数
maxInitialRequests: 10,
//打包规则
cacheGroups: {
//第三方依赖库
vendor: {
test: /[\\/]node_modules[\\/]/,//正则匹配node_modules的文件
name:'vendor',//模块名称
priority: 20,//优先级,数字越大,优先级越高,因为有可能命中多个打包规则,所以需要一个优先级
enforce: true,//是否强制执行
reuseExistingChunk: true//是否重用已有的公共 chunk
},
//公共模块
common: {
name: 'common',
minChunks: 2,//被两处地方引用则被归位公共模块,会单独打包出来
minSize: 1,//最小分割文件大小(1 byte)
priority: 10,
reuseExistingChunk: true
}
}
}
HMR原理:
图片
webpack-dev-middleware
- 将webpack编译后的文件储存到内存当中
- 支持监听和实时编译
- 处理静态资源请求
webpack-hot-middleware
- 建立WebSocket连接
- 发送模块更新信息
- 处理更新失败的情况
HotModuleReplacementPlugin
- 生成HMR运行时代码
- 创建模块热替换的API
- 管理模块更新的状态
流程: A [文件修改] --> B[webpack 检测变化] B [ webpack 检测变化 webpack-dev-middleware监听 ] B --> C[ 重新编译模块 webpack compiler进行编译 ] C --> D[生成新的模块 hash HotModuleReplacementPlugin生成 ] D --> E[通过 WebSocket 发送更新信息 webpack-hot-middleware处理 ] E --> F[浏览器接收更新 HMR Runtime接收 ] F --> G[下载新模块代码 HMR Runtime请求新模块 ] G --> H[替换旧模块HMR Runtime执行模块替换 ]
1、给webpack中的入口文件建立通道连接(通知能力 -- HMR客户端)
Object.keys(baseConfig.entry).forEach(entryItem => {
//第三方包不作为 hmr 入口
if (entryItem !== 'vendor') {
baseConfig.entry[entryItem] = [
//主入口文件
baseConfig.entry[entryItem],
//hmr 更新入口,官方指定的 hmr 路径
`webpack-hot-middleware/client?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMR_PATH}&timeout=${DEV_SERVER_CONFIG.TIMEOUT}&reload=true`
]
}
})
2、启动了一个devServer之后开始监听地址中的代码变化
//引用 devMiddleware 中间件(监控文件改动)
app.use(devMiddleware(compiler, {
//落地到内存的文件:把那些.tpl 文件落地到内存中,因为js等资源会存在本地起的那台后端服务器上
writeToDisk: filePath => filePath.endsWith('.tpl'),
//资源路径
publicPath: webpackDevConfig.output.publicPath,
//headers配置
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'X-Requested-With, Content-Type, Accept, Authorization',
},
stats: {
//颜色打印
colors: true
}
}))
2、当代码修改之后会进行解析编译 --> 模块分包 --> 压缩优化
3、利用webpack-hot-middleware通知HMR客户端来重新拉取最新的代码
//引用 hotMiddleware 中间件(实现热更新通讯)
app.use(hotMiddleware(compiler, {
path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
log: () => {
}
}))
dev与prod的webpack打包差异
development:
- 在打包时需要为入口配置hmr路径,实现热更新
- 可以开启source-map进行代码调试
- output中的publicPath,因为文件是存储在内存中,所以不呢呢个写到具体某个文件,需要配置为服务器地址
production:
- 开启happyPack进行多线程打包
- 可以开启TerserPlugin 删除未使用的代码
懒加载JS文件
首先需要使用动态加载,在路由匹配文件中仅引入加载组件
// 使用动态导入,仅在需要时加载组件 routes.push({ path: '/iframe', component: () => import('./complex-view/iframe-view/iframe-view.vue') })
核心在于分包策略:利用webpack的splitChunks,把第三方包独立出来,然后把平时的公共方法与组件严格按照目录分类去放,在分包的时候控制匹配路径,使业务JS能够独立打包成一个bundle
//公共模块 common: { /** * 动态加载,如果要实现动态加载,那么就要合理分包,因为当包都被打进了common的话,是无法实现部分代码的bundle动态导入的 * 会整个大的导入,同时应该调整目录结构,只对common、widget代码进行common的划分,业务包让他自己单独一个包 */ test: /[\\/common|widgets[\\/]/, name: 'common', minChunks: 2,//被两处地方引用则被归位公共模块,会单独打包出来 minSize: 1,//最小分割文件大小(1 byte) priority: 10, reuseExistingChunk: true },
主要核心在于分包策略:
框架基建建设
前端路由
SSR(服务端渲染):server side rendering
CSR(客户端渲染):client side rendering
npm包的发布
本地调试npm包
1、改名字,package.json 的name的名字(@youzhaorong/elpis)
2、在elpis中执行 npm link
3、在elpis-demo中执行npm link @youzhaorong/elpis
包的发布
首次:npm publish --access public
往后:npm publish
更改文件路径(分为elpis-core的路径/业务路径)
需要更改所有使用到的中间件、拓展、以及路由等目录 (elpis-core/index.js)
工程化
1、需要更改webpack.dev.js、webpack.prod.js、webpack.base.js(文件相关配置)
2、在工程化配置模块部分,写应该使用什么loader的时候,如果直接写'xxx-loader'会在业务项目中寻找,如果业务项目没有安装则会报错。例如:此时需要告诉工程化,应该从elpis-code的node_modules去找,而不是从业务项目的node_modules中找
module: {
rules: [
{
test: /\.vue$/,
use: {
loader: 'vue-loader',//使用的是当前工程的node_modules
}
}
]
}
//使用的是依赖工程(elpis)的node_modules
{
loader: require.resolve('babel-loader')
}
Jenkins
启动:brew services start jenkins
停止:brew services stop jenkins
本地启jenkins,让代码仓库通过webhook通知jenkins构建步骤:
Jenkins基础环境准备:
- brew services start jenkins
- 因为外网不能直接访问内网,所以需要一个内网穿透工具(ngrok),ngrok http <代理端口>,这样就会生成一个公网的访问地址,使得可以访问此链接可直接访问本地的内容
Jenkins项目配置:
- 创建新项目
- 在项目配置中需要启用“触发远程构建”
- 设置认证令牌(也就是coding中的服务URL的token)
- 配置源代码仓库
Jenkins API Token获取:
- 进入用户设置 / Security(安全)/ API Token创建新的Token
- 将用户名(Jenkins 用户 ID: youzr)和API Token进行组合,username:api_token,然后进行base 64的编码
Coding添加Webhook配置:
在项目设置/开发者选项中添加Webhook(也叫Service Hook)
选择对应的代码仓库 与 触发事件
配置服务URL:ngrok生成的公网地址/job/elpis-demo-deploy/build?token=TOKEN_NAME
请求头
javascript Accept: application/json Content-Type: application/json Authorization: Basic <base64编码后的字符串>
Docker
docker中有三大概念
- 仓库:用于存放镜像的仓库
- 镜像:一个只读的模版
- 容器:是镜像的一个运行实例
docker使用的是 client-server
模式,Docker Client 与 Docker Host 之间通过Socket或者RESful API进行通信
- Docker daemon就是服务端的守护进程(负责管理docker资源,也就是电脑上跑起来的Docker app)
图片
使用步骤
- Setp1:创建一个Dockerfile
- Step2:使用Dockerfile出构建一个镜像
- Step3:使用镜像创建和运行容器
Docker指令
镜像指令
检索镜像:docker search
下载镜像:docker pull
删除镜像:
删除指令docker image rmi [选项] <镜像1> [<镜像2> ...]
- <镜像>
可以是
镜像短 ID、
镜像长 ID、
镜像名或者
镜像摘要
删除行为分为两类,一类是
Untagged
,另一类是Deleted
。镜像的唯一标识是其 ID 和摘要,而一个镜像可以有多个标签。因此实际上要求删除某个标签的镜像时,首先会把满足条件的镜像取消(也就是Untagged),当该镜像所有标签都被取消了这个镜像也就失去意义,因此才会触发删除行为。- <镜像>
查看镜像列表:docker images
镜像体积:
- 镜像提及一般会比在Docker hub上的大,那是因为这个是展开后的大小,而Docker hub上面的是压缩后的大小;
- 由于Docker镜像是多层的存储结构,也可以
继承
、复用
不同的基础镜像,相同的层只需要保存一份即可,所以实际上的磁盘占用可能比镜像列表中的大小要小得多
虚悬镜像:
- 查询指令:通过
docker image ls -f dangling=true
查询虚悬镜像
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 00285df0df87 5 days ago 342 MB
- 在镜像列表中有可能存在一类特殊的镜像,他们的存在是因为官方在发布新版本之后,把相同的名字给了新下载的镜像,而旧的镜像被取消变成了
,此类标签已经失去价值可以随意删除 - 删除指令:
docker image prune
中间层镜像:
- 为了加速镜像的构建以及重复利用资源,Docker会利用
中间层镜像
,docker image ls
只能看到顶层的镜像,如果想看到包括中间层的镜像则需要使用docer image ls -a
列出部分镜像:
- 根据仓库名列出镜像:
docker image ls xxx
- 过滤器参数--filter,简写 -f
构建镜像:docker build
docker build
可根据docker file构建出相对应的镜像实例;
使用 docker image ls
可以罗列出所有已经下载的镜像,其中包含 仓库名
、标签
、镜像 ID
、创建时间
以及 所占用的空间
。
镜像构建上下文(context)
在docker build -t nginx:v3 .
中,最后面有一个.
,这个点表示指定上下文路径,在运行 docker build
命令构建镜像的时候其实并非在本地构建,而是在服务端进行(docker engine服务端守护进程); 那么就需要让服务端获取本地文件,这就引入了上下文的概念。
当构建的时候用户需要指定构建上下文路径,docker build命令指定上下文路径之后,会将路径下的所有内容进行打包,然后上传到Docker引擎,这样Docker引擎收到这个上下文包之后展开就能获得构建镜像需要的文件。
例子:如果在Dockerfile中写
docker build -f /project/docker/Dockerfile /project
代表的是
- 指定的DockerFile路径:"/project/docker/Dockerfile"
- 上下文路径:"当前执行命令的位置下的/project下"
注意:这里的Dockerfile路径与Docker构建上下文的路径都是基于构建命令时所在的路径
以下时不同目录执行docker build的情况
/d/coding/elpis-demo/ # 项目根目录 ├── src/ ├── publish/ │ └── beta/ │ └── Dockerfile └── other_files/
1、项目根目录 /d/coding/elpis-demo 下执行
docker build -f publish/beta/Dockerfile .
-f publish/beta/Dockerfile:从当前目录 /d/coding/elpis-demo 找 publish/beta/Dockerfile
.:使用当前命令所在目录作为构建上下文
2、在 publish/beta 目录下执行
docker build -f Dockerfile ../..
-f Dockerfile:从当前目录(publish/beta)找 Dockerfile
../..:构建上下文设为上两级目录(项目根目录 /d/coding/elpis-demo)
3、在任意其他目录(比如 /home/user)执行
docker build -f /d/coding/elpis-demo/publish/beta/Dockerfile /d/coding/elpis-demo
需要使用绝对路径指定 Dockerfile 位置
需要使用绝对路径指定构建上下文
镜像数据持久化:
当停止一个容器的时候,会导致数据的缺失,如果想重启容器的时候让数据持久化有两种方案
挂载目录:
- 一般用于开发调试,因为它直接操作主机文件,容器里面的改动会实时映射到主机上
- 容器运行时会将宿主机的路径挂载进去(前提是宿主机路径存在,或有权限创建)。
- 命令使用:使用-v
/宿主机路径:容器路径
# 把宿主机中的/app/nghtml目录挂载到容器的/usr/share/nginx/html中
docker run -v /app/nghtml:/usr/share/nginx/html nginx
- 容器启动的时候会自动创建“目录”,将宿主机上的目录(如 /app/nghtml)挂载到容器内的目录(如 /usr/share/nginx/html)
卷映射:
- 使Docker自己管理自己的卷,位置在
/var/lib/docker/volumes/<volume-name>
中 - 更适合生产环境使用,让容器与主机解耦
- 命令使用:使用-v
卷名:容器路径进行挂载
# docker创建一个ngconf的卷,并且把容器目录/etc/nginx映射到ngconf这个卷中;这样当nginx读取/etc/nginx目录下的配置的时候就会去读取ngconf卷的数据
docker run -v ngconf:/etc/nginx
容器指令:
- 创建启动一个容器:docker run
- -d:后台启动并返回容器id(一般用于长期服务:如web服务、数据库);如果不加-d会日志会一直占用着控制台
- -it:以交互式的方式启动容器
- -i为让容器保持打开,即使没有连接,这样可以有类似shell的体验
- -t为分配一个伪终端
- (-i 只有输入,没有终端,输出不美观,无法用方向键等。-t 只有终端,没有输入,无法交互,只能看。)
- -p:端口映射<宿主机端口>:<容器机端口>
- --name:容器别名
- --rm:容器停止后自动删除容器
- -e/--env:设置环境变量
- -v:挂载卷
查看容器运行状态:docker ps
- -a:显示所有容器、包含已经停止的
- 启动一个/多个已经创建的容器:docker run
- 停止一个/多个已经创建的容器:docker stop
- 重启容器:docker restart
- 查看容器状态:docker stats
- 查看容器日志:docker logs
- 进入容器:docker exec
dockerfile定制镜像
FROM指令基础镜像
所谓的基础惊喜那个是以一个镜像为基础,在其上进行定制。
scratch
是一个特殊的镜像(虚拟的概念,并不存在),如果你以scratch
作为基础镜像,意味着你不以任何镜像作为基础FROM node:18
RUN执行指令
RUN可以用于执行命令行命令,但要注意的是,Dockerfile会把每一个指令建立一层,所以RUN也不例外,如果按照写法一的话,那么会导致非常多层镜像导致增加部署的时间。如果所有的命令只有一个目的,那么应该只保留一个RUN。使用&&
把各个所需的命令串联起来,Dockerfile支持shell类的行为添加 \
的命令换行
# 如果这样多个RUN就会创建了四层的镜像
FROM debian:stretch
RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN mkdir -p /usr/src/redis
# 正确的写法是
FROM debian:stretch
RUN set -x; buildDeps='gcc libc6-dev make wget' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& mkdir -p /usr/src/redis
COPY命令
COPY [--chown=
- 源路径是:相对于
构建上下文的路径
- 目标路径是:容器内的绝对路径或相对路径(/:总是相对于容器的根目录)
如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径
ADD命令
ADD
指令和 COPY
的格式和性质基本一致
<源路径>可以是一个URL,此时Docker引擎会去下载这个链接放到<目标路径>中;
如果<源路径>是一个tar
压缩文件压缩格式为gzip
、bzip2
以及xz
的话,ADD会自动解压缩到<目标路径>中去;
在 COPY
和 ADD
指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY
指令,仅在需要自动解压缩的场合使用 ADD
。
ENV 设置环境变量
ENV
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
ARG构建参数
ARG构建参数的作用与ENV
相似,都是设置环境变量,不同的是ARG参数所设置的构建环境的环境变量,在容器运行的时候是不会存在这些环境变量的而ENV会一直存在;
作用:Dockerfile中的ARG指令是
定义参数名
以及指定默认值
用法:可以在执行构建命令
docker build
中用--build-arg <参数名>=<值>
来进行覆盖,因此可以在不修改Dockerfile的情况下构建出不同的镜像
# 开发环境构建
docker build --build-arg NODE_ENV=development --build-arg PORT=8080 -t myapp:dev .
# 生产环境构建
docker build --build-arg NODE_ENV=production --build-arg PORT=3000 -t myapp:prod .
- 有效范围:ARG指令生成的构建参数有生效范围,如果在FORM之前定义,那么只能在FORM中使用
# 在 FROM 之前定义
ARG VERSION=latest
FROM node:${VERSION}
# 在 FROM 之后定义
ARG APP_HOME=/app
WORKDIR ${APP_HOME}
# 多阶段构建中的作用域
FROM node:14 as builder
ARG ENV=production # 只在 builder 阶段可用
FROM nginx
ARG ENV=production # 需要在新阶段重新定义
EXPOASE暴露端口
格式为 EXPOSE <端口1> [<端口2>...]
EXPOSE
指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明而开启这个端口的服务;
- 主要功能是用于帮助镜像使用者理解镜像服务的守护端口,方便配置映射
EXPOSE
和 docker run -P <宿主端口>:<容器端口>
是不一样的,容器端口是将对应端口服务公开给外界访问的,而 EXPOSE
只是单纯声明容器打算用什么端口
WORKDIR工作目录
使用 WORKDIR
指令制定工作目录,以后 各层的当前目录就被改为指定的目录,如果该目录不存在,WORKDIR会帮你建立目录。
LABEL为镜像添加元数据
LABEL
指令用来给镜像以简直对的形式添加一些元数据,例如:
为镜像添加描述性信息
添加版本信息
添加维护者信息
添加其他自定义信息
LABEL <key>=<value> <key>=<value> <key>=<value>...
ENTRYPOINT主程序命令
容器的「主程序」,容器启动时一定会执行它,相当于给容器写一个启动命令
ENTRYPOINT npm run prod
Kubernetes
Kubernetes集群
Kubernetes的集群分为分为主集群(master)、与工作集群(work)
- Maste主集群负责集群中的管理、调度。
- Worker节点为虚拟机或者物理计算机,每个Worker工作节点都有一个Kubelet,他管理当前节点与主节点间的通讯
Kubernetes部署(deployment)
在Kubernetes中,可以通过发布Deployment来创建应用程序(docker image)的实例(docker container),这个实例会被包含在 Pod
中,Pod
为Kubernetes的最小管理单元。
在K8S集群发布Deployment之后,Deployment将指示K8S如何创建和更新应用实例,master节点将应用程序实例调度到集群的具体工作节点上。
创建了应用程序实例之后,Kubernetes Deloyment Controller将持续监控这些实例,如果实例运行的worker节点关机或者被删除,那么Kubernetes Deployment Controller将在集群中寻找资源最优的一个worker节点重新创建一个应用程序实例。提供了一种自我修复机制来解决机器故障或维护的问题。
Deployment位于Master节点中,发布Deployment后,master会在合适的worker节点创建Containers(图中的正方体),Container会被包含在Pod中(蓝色圆圈)。
使用Kubectl部署
apiVersion: apps/v1 # Kubernetes API 版本,apps/v1 用于 Deployment 资源
kind: Deployment # 资源类型:Deployment(部署),用于管理 Pod 的生命周期
metadata: # 元数据信息,包含资源的基本信息
namespace: elpis-demo-prod # 命名空间,类似文件夹,用于隔离不同环境的资源
name: ed-prod-deployment # Deployment名称,在同一命名空间内必须唯一
labels: # 标签,用于标识和分类资源
app: ed-prod # 给这个 Deployment 贴上 app=ed-prod 的标签
# Deployment 的详细规格配置
spec:
replicas: 1 # 使用该Deployment 创建多少个应用程序实例
selector: # 选择器,用于确定这个 Deployment 管理哪些 Pod
matchLabels: # 通过标签匹配的方式选择 Pod
app: ed-prod-pod # 管理所有带有 app=ed-prod-pod 标签的 Pod
# Pod 模板,定义要创建的 Pod 的模版
template:
# Pod 的元数据
metadata:
labels: # 给 Pod 贴标签
app: ed-prod-pod # Pod 的标签,必须与上面的 selector 匹配
# Pod 的详细规格
spec:
containers: # 容器列表
- image: 'elpis-demo:latest' # Docker 镜像名称
name: ed-prod-container # 容器的名称
imagePullPolicy: Never # 镜像拉取策略:Never=只使用本地镜像,不从远程仓库拉取
ports: # 容器端口配置
- containerPort: 8888 # 容器内部监听的端口号
应用 deployment.yaml 文件
kubectl apply -f deployment.yaml文件路径
路径类型 | 示例 | 说明 |
---|---|---|
绝对路径 | /publish/prod/deployment.yaml | 这是从系统根目录(/)开始查找的路径,必须是文件系统中实际存在的完整路径 |
相对路径 | publish/prod/deployment.yaml | 从当前终端所在目录(Current Working Directory)开始查找 |
查看Pod状态
kubectl get pods -n xxx #如果不写-n默认查看default的namespace
Pods
在应用了 Deployment 之后,Kubernetes创建了一个 Pod
(容器组)来放置应用程序实例(docker实例)
Pod容器组
是一个K8s的抽象概念,用于存放一组Container(可能包含一个也可能包含多个,即图中的正方体),以及这些Container(容器)的一些共享资源:
- 共享存储:卷(Volumes)紫色的圆柱;
- IP地址:每个Pod在自己的
Namespace
有唯一的IP地址,Pod容器组中的所有Container(容器)共享该IP地址;Pod容器组中各个容器的端口不能冲突,容器间可使用localhost + 端口号
互相访问 - Container容器的基本信息:容器的镜像版本,对外暴露的端口号
Pod是K8s的最基本单位,当在K8S中 创建Deployment的时候 会在集群上 创建包含容器的Pod(而不是直接创建容器)
每个Pod都与他运行的 worker节点
绑定,并保持在那直至终止或删除。如果工作节点发生故障,则会在集群中其他可用的工作节点运行相同的Pod(从同样的镜像创建Container、使用同样的配置、IP地址不同、Pod名字不同)
Worker Node
Pod容器组总是在Worker Node中运行,Worker Node是K8S集群中的计算机,每个Worker Node都由Master主集群管理,一个工作节点可以包含多个容器组,Kubernetes Master主集群会根据每个Work Node上的可用资源的情况,去 自动调度Pod容器
到最佳的Work Node上
每个Kubernetes Node至少运行:
- Kubelet:负责Master节点与Worker节点之间的通信工程;
- 容器运行环境:例如Docker,负责下载镜像,创建以及运行容器
Kubectl常用命令
# 获取类型为Deployment的资源列表
kubectl get deployments
# 在命令后增加 -A 或 --all-namespaces 可查看所有 名称空间中 的对象
# 使用参数 -n 可查看指定名称空间的对象
kubectl get deployments [-A] #与kubectl get deployments --all-namespaces相等
kubectl get deployments [-n] elpis-prod
# 获取类型为Pod的资源列表
kubectl get pods [-n]
# 获取类型为Node的资源列表
# 列出当前 Kubernetes 集群中的所有节点(Nodes)信息。这些节点包括:
# • Master 节点(控制平面节点):负责调度、管理、协调。
# • Worker 节点(工作节点):运行你的 Pod(也就是实际运行应用的地方)。
kubectl get nodes [-n]
#describe
kubectl describe pod [pod名称] [不加就默认查看default的namespace] #显示Pod相关资源的详细信息
kubectl describe deployment -n [不加默认查看default的namespace] #查看Deployment信息
#logs
# 查看日志 kubctl logs pod名称
# -f --follow 持续日志
kubectl logs -f pod
Service
Pod容器组有自己的生命周期,当Worker Node节点故障的时候,节点上的Pod也会消失,此时Deployment可以通过创建新的Pod动态将集群调整回原有状态保障运行;
例子:例如有一个处理程序,此时有三个运行时副本,这三个副本是可以替换的,即使Pod容器组 消失并被重建 或者副本数量从 三个变成五个 (扩缩容机制)
;此时前端应该无需关心这些后端系统的副本变化;
由于Kubernetes集群每个Pod都有一个唯一的IP地址,因此需要一种机制,需要为前端系统屏蔽后端系统(Pod容器组)在新建、销毁Pod过程中所带来的IP地址的变化
;Service服务就提供了这样的一个抽象层,他具备 选择某些特征的Pod并为其提供一个访问方式 的能力。一个Service选择某些特征的Pod通常由 LabelSelector (选择标签)
决定。
创建Service的时候(service.yaml)通常设置配置文件中的 spec.type
值,可以以不同的方式向外部暴露程序;
ClusterIP(default)
- 在集群内部IP公布服务,这种方式的Service只能在在集群内部访问
NodePort
- 使用NAT在集群中的每个工作节点的同一端口上公布服务,这种情况下集群中任意节点可通过
: 去访问服务,此时ClusterIP服务仍可用
- 使用NAT在集群中的每个工作节点的同一端口上公布服务,这种情况下集群中任意节点可通过
LoadBalancer
- 云环境中(需供应商支持)创建一个集群外部的负载均衡器,且以使用负载均衡器的IP地址作为服务的访问地址,此时ClusterIP和NodePort的方式仍然可用
利用 scaling 实现扩缩容
当deployment的replicas为1的时候只有1个pod,那么流量转发只能转发到这个pod中;如果修改了 replicas
之后,会增加pod的数量,从而流量的负载均衡在这几个pod中进行转发;
module_05_scaling1.d9d22450
module_05_scaling2.3f74dfba
一但运行了多个应用实例,就可以在不停机的情况下进行滚动更新
执行滚动更新
如果用户希望的是程序始终可用,那么更新程序时需要分多次运行,在K8S中是通过Rolling Update进行更新的,Rolling Update滚动更新
是通过使用 新版本的Pod 逐步替代 旧版本的Pod 来实现 Deployment 的更新;从而实现不停机更新;
Kubernetes 更新多副本的 Deployment 的版本时,会逐步的创建新版本的 Pod,逐步的停止旧版本的 Pod;以便使应用一直处于可用状态。这个过程中,Service 能够监视 Pod 的状态,将流量始 终转发到可用的 Pod 上。
过程如下:
1. 初始状态:
原本 Service A 将流量负载均衡到 4 个旧版本的 Pod (当中的容器为 绿色)上
2. 根据新版本的镜像创建新的Pod
更新完 Deployment 部署文件中的镜像版本后,master 节点选择了一个 worker 节点,并根据新的镜像版本创建 Pod(紫色容器)。
新 Pod 拥有唯一的新的 IP。同时 master 节点选择一个旧版本的 Pod 将其移除。此时,Service A 将新 Pod 纳入到负载均衡中,将旧Pod移除;直至全部旧版本的Pod被移除,新版本的Pod达到 Deployment 部署文件中定义的副本数量,Rolling update完成
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 4
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.8 #使用镜像nginx:1.8替换原来的nginx:1.7.9
ports:
- containerPort: 80
通过minikube启动service
执行 minikube service -n elpis-demo-prod ed-prod
可以访问 在本地访问Kubernetes集群中的Service。
- 如果 Service 的type是
NodePort
或者LoadBalancer
它会自动做一个端口映射;把nodePort映射到Service的port中; - 第一段的意思是在
minikube
这个虚拟机中运行的地址; - 第二段的意思是:要打开主机与虚拟机的一个访问通道
- minikube是Docker驱动,运行在一个虚拟的网络中,并不能直接通过我的主机进行监听;所以Minikube自动开启一个主机本地的端口转发,把
http://127.0.0.1:60289
这个主机地址映射到虚拟机环境下的http://192.168.49.2:32507
中;
- minikube是Docker驱动,运行在一个虚拟的网络中,并不能直接通过我的主机进行监听;所以Minikube自动开启一个主机本地的端口转发,把
youzhaorong@KeganRayMac-mini elpis-demo % minikube service -n elpis-demo-prod ed-prod
i|-----------------|---------|-------------|---------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------------|---------|-------------|---------------------------|
| elpis-demo-prod | ed-prod | 8888 | http://192.168.49.2:32507 |
|-----------------|---------|-------------|---------------------------|
🏃 为服务 ed-prod 启动隧道。
|-----------------|---------|-------------|------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------------|---------|-------------|------------------------|
| elpis-demo-prod | ed-prod | | http://127.0.0.1:60289 |
|-----------------|---------|-------------|------------------------|
🎉 正通过默认浏览器打开服务 elpis-demo-prod/ed-prod...
流量流转过程:
- 外部流量进入
- 公网负载均衡器(腾讯云做的一层)
- K8S的某个工作节点的NodePort
- K8S Service
- K8S Service做的一层负载均衡(其实背后是kube-proxy做的负载均衡)
- Pod
CI(持续集成)
--------------------------- CI持续集成 开始👺 ---------------------------
- Jenkins启动并准备环境(从代码仓库拉取代码)
- 前端构建阶段
- 安装依赖(npm install)
- 构建生产环境(npm run build:prod)
- Docker制作镜像
- 设置docker镜像构建上下文(一般为Jenkins的当前环境)
- 指定运行环境(node版本)
- 设置工作目录
- 把构建上下文的前端代码拷贝到工作目录
- 暴露端口
- 指定容器启动命令(启动后端服务)npm run (beta/prod)
- 将镜像推送到制品仓库
- docker push 腾讯云的制品仓库
--------------------------- CI持续集成 完成✅ ---------------------------
CD(持续部署)
--------------------------- CD持续部署 开始👺 --------------------------
环境准备
- Docker - 容器化应用
- Minikube - 本地的Kubernetes集群
- Jenkins - CI/CD自动化工具
- NodeJS - 应用运行环境
配置文件的准备
Dockerfile
- 基于Node.js18镜像(有可能在Jenkins部署的时候会拉取不到镜像,这里的做法是在本地docker拉取镜像,然后在deployment的时候强制使用本地的docker镜像)
Kubernetes配置
deployment.yaml - 定义Pod部署配置
设置imagePullPolicy: Never确保使用本地镜像
定义容器端口(8080)
service.yaml - 定义服务访问配置
端口映射(8080→NodePort)
类型为LoadBalancer
镜像加载 - 将镜像加载到Minikube
Kubernetes部署 - 应用deployment 和 service的部署
部署验证 - 等待rollout完成 ,确认pod状态
--------------------------- CD持续部署 完成✅ ---------------------------
#Dockerfile
# 配置 环境基础镜像
FROM docker.io/library/node:18
LABEL authors="youzhaorong"
# 创建工作目录
RUN mkdir -p /app/elpis-demo
# 进入工作目录
WORKDIR /app/elpis-demo
# 因为前面的工作是安装相关依赖 以及打包前端项目 所以现在目录是停留在dist目录的,复制dist目录的代码内容到工作目录
COPY . /app/elpis-demo
# 设置环境变量
ENV TimeZone=Asia/Shanghai
# 暴露端口
EXPOSE 8888
ENTRYPOINT npm run prod
#Deployment.yaml
apiVersion: apps/v1 # Kubernetes API 版本,apps/v1 用于 Deployment 资源
kind: Deployment # 资源类型:Deployment(部署),用于管理 Pod 的生命周期
metadata: # 元数据信息,包含资源的基本信息
namespace: elpis-demo-prod # 命名空间,类似文件夹,用于隔离不同环境的资源
name: ed-prod-deployment # Deployment 的名称,在同一命名空间内必须唯一
labels: # 标签,用于标识和分类资源
app: ed-prod # 给这个 Deployment 贴上 app=ed-prod 的标签
# Deployment 的详细规格配置
spec:
replicas: 1 # 副本数量,即要运行多少个相同的 Pod
selector: # 选择器,用于确定这个 Deployment 管理哪些 Pod
matchLabels: # 通过标签匹配的方式选择 Pod
app: ed-prod-pod # 管理所有带有 app=ed-prod-pod 标签的 Pod
# Pod 模板,定义要创建的 Pod 的规格
template:
metadata: # Pod 的元数据
labels: # 给 Pod 贴标签
app: ed-prod-pod # Pod 的标签,必须与上面的 selector 匹配
spec: # Pod 的详细规格
containers: # 容器列表,一个 Pod 可以包含多个容器
- image: 'elpis-demo:latest' # Docker 镜像名称和标签
name: ed-prod-container # 容器的名称
imagePullPolicy: Never # 镜像拉取策略:Never=只使用本地镜像,不从远程仓库拉取
ports: # 容器端口配置
- containerPort: 8888 # 容器内部监听的端口号
#Service.yaml
apiVersion: v1 # Kubernetes API 版本,v1 用于 Service 资源
kind: Service # 资源类型:Service(服务),用于暴露 Pod 供外部访问
metadata: # 元数据信息,包含资源的基本信息
namespace: elpis-demo-prod # 命名空间,必须与 Deployment 在同一命名空间
name: ed-prod # Service 的名称,在同一命名空间内必须唯一
spec: # Service 的详细规格配置
ports: # 端口映射配置列表
- port: 8888 # Service 对外暴露的端口号
targetPort: 8888 # 转发到 Pod 容器的目标端口号
selector: # 选择器,决定流量转发给哪些 Pod
app: ed-prod-pod # 选择标签为 app=ed-prod-pod 的 Pod(与 Deployment 中的 Pod 标签匹配)
type: LoadBalancer # Service 类型:LoadBalancer=负载均衡器,可从集群外部访问
#Jenkinsfile
#意思是以bin/bash交互启动
#!/bin/bash
# 如果遇到错误终止整个程序
set -e
echo "=== 步骤0: 确认环境 ==="
docker --version
node --version
kubectl version --client
minikube version
echo "=== 步骤1: 安装依赖 ==="
npm install
echo "=== 步骤2: 构建前端项目 ==="
npm run build:prod
echo "=== 步骤3: 检查node:18镜像是否存在 ==="
if ! docker images | grep -q "node.*18"; then
echo "正在拉取node:18镜像..."
docker pull node:18
else
echo "node:18镜像已存在,继续构建..."
fi
# 显示当前所有镜像
docker images | grep node
echo "=== 步骤4: 构建Docker镜像 ==="
# 确保Dockerfile中使用完整路径
echo "正在使用已有的node:18镜像构建..."
docker build -t elpis-demo:latest -f publish/prod/Dockerfile .
echo "=== 步骤5: 确保minikube运行 ==="
minikube status || minikube start
echo "=== 步骤6: 加载镜像到minikube ==="
echo "将elpis-demo:latest镜像加载到minikube中..."
minikube image load elpis-demo:latest
echo "=== 步骤7: 创建namespace(如果不存在)==="
kubectl create namespace elpis-demo-prod || true
echo "=== 步骤8: 应用Kubernetes配置 ==="
kubectl apply -f publish/prod/deployment.yaml
kubectl apply -f publish/prod/service.yaml
echo "=== 步骤9: 等待部署完成 ==="
kubectl rollout status deployment/ed-prod-deployment -n elpis-demo-prod --timeout=90s || {
echo "部署未完成,查看pod状态..."
kubectl get pods -n elpis-demo-prod
kubectl describe pods -n elpis-demo-prod
echo "查看minikube上的镜像..."
minikube ssh "docker images | grep elpis-demo"
exit 1
}
echo "=== 步骤10: 获取服务访问地址 ==="
# 不使用会阻塞的minikube service命令,而是直接获取IP和端口信息
MINIKUBE_IP=$(minikube ip)
NODE_PORT=$(kubectl get service ed-prod -n elpis-demo-prod -o jsonpath='{.spec.ports[0].nodePort}')
LOCAL_URL="http://127.0.0.1:${NODE_PORT}"
CLUSTER_URL="http://${MINIKUBE_IP}:${NODE_PORT}"
echo "服务已部署成功!"
echo "集群内访问地址: ${CLUSTER_URL}"
echo "本地访问地址: ${LOCAL_URL}"
echo "要访问服务,请在终端中运行: minikube service ed-prod -n elpis-demo-prod"
echo "(注意:在 macOS 上需要手动访问该地址)"
echo "CI/CD部署完成!"
# 明确退出脚本,不再阻塞Jenkins构建
exit 0
service.js、Dockerfile、deployment.yaml、service.yaml各个文件中端口的区别
- service的端口号代表项目启动的端口号,这里应该是 容器内部实际监听 的端口号
- Dockerfile中的端口只是声明容器会监听哪个端口,这个端口应该和实际项目运行的端口一致
- 例如项目运行在3000,Dockerfile就应该写EXPOSE 3000
- Kubernetes的 Deployment.yaml
- ports:
- containerPort:这个端口为容器内部监听的端口 应该与实际的端口一致
- ports:
- Kubernetes的 Service的 port 与 targetPort
- port:Service对外暴露的端口(集群内访问用),可以和targetPort一样,也可以不一样
- targetPort:pod里面容器实际监听的端口,必须和Deployment里面的containerPort、项目运行的端口一致