ZTeam's Elpis Note

6 天前
4

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} 匹配 abc

//例子:例如这里拿到了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。它们的配置项都是相同,作用也是一样的,只需要选择其中一种。一般来说它们的配置项其实都是 pluginspresets

  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-polyfillcore-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}`) } }
      
      
      当调用renderPage的时候上下文会多一个render属性,把文件路径拼接到渲染引擎路径后面
    
    - 注册全局中间件
    
        - 使用了模版渲染引擎noa-nunjucks-2这个中间件
    
        - 设置渲染tpl路径,/app/public为目录
    
        -
    javascript app.use(koaNunjucks({ ext:"tpl", path: path.resolve(process.cwd(),'./app/public'),//渲染根目录 nunjucksConfig: { nocache:true, trimBlocks:true } }))
    
    - 注册路由
    
      -
    javascript router.get('/view/:page', viewController.renderPage.bind(viewController))
    
    
    当用户访问路由view/page1,触发此路由文件,:page为变量page1,就会去调用对应的controller的renderPage方法,而renderPage方法会访问的文件路径则为 `渲染引擎路径`+`render路径` = `./app/public/output/entry.page1.tpl` 1、路由
    2、controller
      
      
    3、tpl文件 ```

接口访问流程

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 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压缩文件压缩格式为gzipbzip2以及xz的话,ADD会自动解压缩到<目标路径>中去;

COPYADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 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 指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明而开启这个端口的服务;

  • 主要功能是用于帮助镜像使用者理解镜像服务的守护端口,方便配置映射

EXPOSEdocker 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文件路径
`kubectl命令中的路径` 支持两种形式:
路径类型示例说明
绝对路径/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服务仍可用
  • LoadBalancer

    • 云环境中(需供应商支持)创建一个集群外部的负载均衡器,且以使用负载均衡器的IP地址作为服务的访问地址,此时ClusterIP和NodePort的方式仍然可用

利用 scaling 实现扩缩容

当deployment的replicas为1的时候只有1个pod,那么流量转发只能转发到这个pod中;如果修改了 replicas 之后,会增加pod的数量,从而流量的负载均衡在这几个pod中进行转发;

module_05_scaling1.d9d22450

module_05_scaling1.d9d22450
module_05_scaling2.3f74dfba

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 中;
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:这个端口为容器内部监听的端口 应该与实际的端口一致
  • Kubernetes的 Service的 port 与 targetPort
    • port:Service对外暴露的端口(集群内访问用),可以和targetPort一样,也可以不一样
    • targetPort:pod里面容器实际监听的端口,必须和Deployment里面的containerPort、项目运行的端口一致

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...